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
- Què són els type hints i, sobretot, què NO fan
- Sintaxi bàsica: variables, paràmetres i valor de retorn
- Tipus genèrics moderns:
list[Llibre],dict[str, Llibre],tuple,set | NoneiOptional: el contracte decercar_llibre()Unionamb|: quan una cosa pot ser diverses coses- Anotar
magatzem.py: les signatures reals de Papyrus TypedDict: donar forma a les files de CSV i JSONmypy: el verificador extern- 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:
preu: float— el paràmetre hauria de ser unfloat.-> float— la funció retorna unfloat.
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 cridaL'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 = TrueEn 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":
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:
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):
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 impedeixvendre("dos"). Per rebutjar dades en execució continuen calent les guard clauses de 07-03. Els hints avisen abans; elsraiseprotegeixen durant. - Oblidar el
| Noneen funcions que poden no trobar: sicercar_llibres'anota-> Llibre, menteixes al lector i amypy, que deixarà passar l'AttributeError. L'anotació ha de dir tota la veritat. lista seques en comptes delist[Llibre]: és legal però perd gairebé tot el valor — l'editor no sabrà autocompletar els elements.- Importar
List/Optionaldetypingper inèrcia: en Python modern (3.10+),list[...]iX | Nonesón la forma preferida. Reconeix la vella quan la llegeixis; escriu la nova. - Consell: executa
mypycom a part de la teva rutina, igual que mirespapyrus.log. Un error demypyé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
-
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
Nonesi el catàleg és buit:def mes_car(cataleg): if not cataleg: return None return max(cataleg.values(), key=lambda l: l.preu).titol -
Defineix un
TypedDictanomenatFilaCatalegper a les files del CSV del catàleg (claustitol,preu,estoc, totesstrperquè venen deDictReader) i anota la funciófila_a_llibre(fila)que converteix una fila en unLlibre(pot llançarValueErrorsi el preu no és numèric — recorda: això va a la docstring, no al->). -
Aquest codi passa desapercebut fins que l'Omar demana un títol esgotat. Quin error assenyalaria
mypyi 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
-
def mes_car(cataleg: dict[str, Llibre]) -> str | None: if not cataleg: return None return max(cataleg.values(), key=lambda l: l.preu).titolEl
-> str | Noneés obligatori: hi ha un camí que retornaNone. Anotar només-> strseria mentir. -
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 quetancament_de_caixaprotegeix amb el seutrymínim (07-02); l'excepció possible es documenta a la docstring. -
mypyassenyalaItem "None" of "Llibre | None" has no attribute "hi_ha_estoc":cercar_llibrepot retornarNoneiNone.hi_ha_estoc()petaria. Dos arranjaments coherents amb M7: comprovarif llibre is None: raise LlibreNoTrobatError(titol)abans de fer-lo servir, o directament cridarobtenir_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
- Introducció a Python
- Configuració de l'Entorn de Desenvolupament
- Sintaxi de Python i Tipus de Dades Bàsics
- Variables i Constants
- Entrada i Sortida Bàsica
- Entorns Virtuals i Gestió de Paquets
Mòdul 2: Estructures de Control
Mòdul 3: Funcions i Mòduls
- Definició de Funcions
- Arguments de Funció
- Funcions Lambda
- Mòduls i Paquets
- Visió General de la Biblioteca Estàndard
Mòdul 4: Estructures de Dades
Mòdul 5: Programació Orientada a Objectes
Mòdul 6: Gestió de Fitxers
- Lectura i Escriptura de Fitxers
- Treball amb Fitxers CSV
- Gestió de Dades JSON
- Operacions amb Fitxers i Directoris
Mòdul 7: Gestió d'Errors i Excepcions
- Introducció a les Excepcions
- Gestió d'Excepcions
- Llançament d'Excepcions
- Excepcions Personalitzades
- Bones Pràctiques i Registre d'Errors amb logging
Mòdul 8: Temes Avançats
- Anotacions de Tipus (type hints)
- Decoradors
- Generadors
- Gestors de Context
- Concurrència: Fils i Processos
- Asyncio per a la Programació Asíncrona
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb unittest
- Proves amb pytest
- Desenvolupament Guiat per Proves
- Tècniques de Depuració
- Ús de pdb per a la Depuració
Mòdul 10: Desenvolupament Web amb Python
- Introducció al Desenvolupament Web
- Fonaments del Framework Flask
- Construcció d'APIs REST amb Flask
- Introducció a Django
- Construcció d'Aplicacions Web amb Django
Mòdul 11: Ciència de Dades amb Python
- Introducció a la Ciència de Dades
- NumPy per a la Computació Numèrica
- Pandas per a la Manipulació de Dades
- Matplotlib per a la Visualització de Dades
- Introducció a l'Aprenentatge Automàtic amb scikit-learn
