Benvenuto alla seconda lezione. Saltiamo il prologo “hello world” con cui la maggior parte dei tutorial Python apre, perché se stai leggendo la seconda lezione di questo corso sai già scrivere uno print e abbiamo cose migliori da fare. Cominciamo invece dal singolo cambiamento culturale più grande in Python nell’ultimo decennio: i type hints hanno smesso di essere opzionali.
Nel 2014, quando PEP 484 ha introdotto il typing in Python, era un’idea controversa. Metà della community era entusiasta di avere finalmente il controllo statico; l’altra metà scriveva post arrabbiati su come il typing fosse un tradimento dell’anima dinamica di Python. Nel 2026 quella discussione è chiusa. Ogni libreria importante è tipizzata. Ogni descrizione di lavoro per ruoli senior in Python menziona mypy o pyright. Ogni code review in un’azienda competente segnalerà un type hint mancante. Gli assistenti AI ti danno suggerimenti drasticamente migliori quando i tipi ci sono. Il runtime continua a non interessarsene: farà girare felicemente Python non tipizzato fino al collasso termico dell’universo, ma gli umani e gli strumenti attorno al runtime ora se ne interessano molto.
Questa lezione è la guida pratica: cosa tipizzare, come tipizzarlo, quando saltarlo, e quali strumenti puntare al tuo codice. Tutto da qui in avanti usa i type hints di default, quindi assicuriamoci di essere allineati.
Perché tipizzare, se il runtime li ignora
I type hints di Python non sono enforced a runtime. Questo spiazza chiunque arrivi da Java, C#, TypeScript, Rust, o davvero qualsiasi altro linguaggio tipizzato. Puoi scrivere questo:
def add(x: int, y: int) -> int:
return x + y
print(add("hello", "world")) # stampa "helloworld". Sì, davvero.
Python vede int e fa spallucce. L’operatore + di Python funziona sulle stringhe, la funzione restituisce una stringa, il programma gira. Il type hint era un commento che l’interprete ha gentilmente ignorato.
Quindi perché preoccuparsene? Quattro motivi, ordinati per quanto spesso ti salvano la pelle nella pratica.
1. Supporto dell’editor. Gli editor moderni (VS Code con Pylance, PyCharm, Cursor, Neovim con pyright-langserver) leggono i type hints e ti danno autocomplete, aiuto sui parametri, click-through alle definizioni, e sottolineature rosse quando usi male una funzione. Senza type hints tutto questo decade a “indovinare”. Una volta che hai lavorato in una codebase pienamente tipizzata non torni più indietro, allo stesso modo in cui chi ha usato un debugger non torna più agli statement print (per lo più).
2. Catturare bug all’edit-time. Strumenti come pyright e mypy ti dicono, prima che tu esegua il codice, che stai passando uno str dove una funzione si aspetta un int, o che una funzione potrebbe restituire None e ti sei dimenticato di gestirlo. Una frazione sorprendente dei bug “veri” nelle codebase Python non tipizzate è esattamente di questa categoria: una funzione che di solito restituisce un valore ma a volte restituisce None, e chi la chiama non ha mai controllato. I type hints rendono impossibile ignorarli.
3. Auto-documentazione. Una firma di funzione come def fetch(client, query, timeout=None, retries=3) non ti dice quasi nulla. La stessa funzione come def fetch(client: HttpClient, query: str, timeout: float | None = None, retries: int = 3) -> Response ti dice esattamente cosa passare e cosa otterrai indietro. La docstring ha ancora del lavoro da fare, ma il contratto è nella firma.
4. Gli assistenti AI diventano drasticamente migliori. Questo è nuovo. Copilot, Claude e gli altri sono tutti allenati su codice tipizzato; più tipi ha la tua codebase, più accuratamente predicono cosa scrivere dopo. Gli errori di tipo entrano nel contesto dell’AI e ne plasmano i suggerimenti. Ci torniamo.
Le basi, sintassi moderna
Se hai imparato il typing da un blog post del 2018, questa sezione ti sembrerà leggermente diversa da come la ricordi. Python 3.9 (sintassi list[int]) e 3.10 (sintassi X | None) hanno ripulito drasticamente l’ergonomia. Useremo le forme moderne ovunque.
# Argomenti di funzione e tipo di ritorno
def greet(name: str) -> str:
return f"Hello, {name}"
# Annotazione di variabile (raramente serve; il tipo di solito viene inferito)
count: int = 0
ratio: float = 0.5
# Tipi container built-in: nota - minuscolo, niente import
names: list[str] = ["Alice", "Bob"]
ages: dict[str, int] = {"Alice": 30, "Bob": 25}
coords: tuple[float, float] = (45.46, 9.19)
unique_ids: set[int] = {1, 2, 3}
# Valori opzionali: preferisci la sintassi `|` a `Optional[X]`
def find_user(user_id: int) -> User | None:
...
# Unione di più tipi
def parse(value: str | int | float) -> Decimal:
...
# Valori di default con tipi
def fetch(url: str, timeout: float = 5.0) -> bytes:
...
Qualche cosa che vale la pena notare:
list[str]nonList[str]. LaListcon la L maiuscola datypingè la forma pre-3.9. Buttala.int | NonenonOptional[int]. Entrambe sono legali; la nuova forma è più corta e non richiede import.tuple[float, float]è una tuple di esattamente due float.tuple[int, ...]è “una tuple di un numero qualsiasi di int” (i...sono davvero la sintassi).- Python ora inferisce i tipi nei casi ovvi.
count = 0va bene;count: int = 0è ridondante. Annota le variabili solo quando il tipo non è ovvio dall’assegnazione, o quando dichiari una variabile senza un valore (buffer: list[bytes] = []va bene;buffer = []lascia il checker insicuro su cosa ci finirà dentro).
Funzioni, callables e iterables
Il codice reale passa funzioni in giro. Il codice reale consuma generators. Tipizza anche quelli.
from collections.abc import Callable, Iterable, Iterator
# Una funzione che prende una funzione
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
# Una funzione che prende qualsiasi cosa iterabile
def total(numbers: Iterable[float]) -> float:
return sum(numbers)
# Una funzione generator
def count_up(stop: int) -> Iterator[int]:
n = 0
while n < stop:
yield n
n += 1
Nota che importiamo da collections.abc, non da typing. typing.Iterable e amici sono alias deprecati a partire dalla 3.9; collections.abc è la casa moderna. Entrambi funzionano; il codice nuovo usa il percorso nuovo.
Callable[[int, int], int] si legge come “un callable che prende due int e restituisce un int”. Se non ti interessa la firma, Callable da solo va bene, ma stai buttando via gran parte del valore di tipizzarlo.
Generics: la nuova sintassi
A volte vuoi una funzione che lavori su “una lista di qualcosa” e restituisca “il qualcosa”. Quel qualcosa è una type variable, e la 3.12 ha reso molto più pulito dichiararle.
# Pre-3.12 (funziona ancora, ancora comune nel codice più vecchio)
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
# 3.12+
def first[T](items: list[T]) -> T:
return items[0]
# Stessa idea, con una classe
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
# Gli alias di tipo hanno avuto lo stesso trattamento
type UserId = int
type Json = dict[str, "Json"] | list["Json"] | str | int | float | bool | None
Usa la sintassi nuova per il codice nuovo. Vedrai ancora la vecchia forma TypeVar nelle librerie scritte prima della 3.12: non è sbagliata, solo verbosa.
Tipi a uso speciale che vale la pena conoscere
from typing import Any, Literal, Final, TypedDict, NotRequired
# Any: la via di fuga. Significa "sto rinunciando al typing per questo valore".
# Usalo con parsimonia; ogni Any è un buco nella tua type safety.
def parse_unknown(blob: Any) -> dict[str, Any]:
...
# Literal: solo valori specifici sono ammessi
def set_mode(mode: Literal["read", "write", "append"]) -> None:
...
# Final: questo nome viene assegnato una volta e mai riassegnato
MAX_RETRIES: Final = 5
# TypedDict: un dict con chiavi note e tipi di valore
class UserDict(TypedDict):
id: int
name: str
email: NotRequired[str] # puo' esserci o non esserci
TypedDict è davvero utile quando stai parsando JSON e non vuoi definire una classe completa. Literal rende più sicuri i parametri “enum di stringhe”. Final è documentazione che cattura anche le riassegnazioni accidentali.
from __future__ import annotations
Vedrai questa riga in cima alle codebase più vecchie:
from __future__ import annotations
Contesto: i type hints venivano valutati al momento della definizione della funzione, il che causava problemi con i forward reference (una classe che si riferisce a se stessa, tipi definiti più avanti nel file, eccetera). Il fix è stato PEP 563, “postponed evaluation of annotations”, che rende tutte le annotazioni stringhe valutate solo quando qualcosa le richiede.
Nella 3.10+ per lo più non ti serve. I forward reference funzionano per lo più. Nella 3.13 resta opt-in ma non è il default; l’approccio originale di PEP 649 “deferred evaluation” è atterrato nella 3.14 e cambia di nuovo il quadro. Per questo corso puoi ignorare l’import __future__ a meno che tu non incappi in un errore specifico di forward reference, nel qual caso aggiungerlo di solito risolve.
Type checker: pyright e mypy
Due opzioni vere.
pyright (Microsoft, scritto in TypeScript, distribuito in VS Code/Cursor come Pylance). Più veloce di mypy di un margine ampio. Default più stringenti. Migliore nel checking incrementale. Se usi VS Code, sta già girando a ogni keystroke. Per eseguirlo da CLI:
uv tool install pyright
pyright src/
mypy (l’originale, gestito dalla typing community di Python). Più configurabile, default più conservativi, migliore in alcuni casi limite con i generics. Per eseguirlo:
uv tool install mypy
mypy src/
Entrambi producono risultati simili sulla maggior parte del codice; entrambi a volte saranno in disaccordo, di solito su quanto essere stringenti sui casi limite. Scegline uno per un dato progetto e configuralo in pyproject.toml. Non farli girare entrambi: discuterai con due strumenti invece di uno.
Un blocco di pyproject.toml ragionevole per partire con pyright:
[tool.pyright]
include = ["src"]
typeCheckingMode = "standard"
pythonVersion = "3.13"
Imposta typeCheckingMode = "strict" quando ti sentirai a tuo agio. È un salto netto nel rumore, ma è dove le codebase serie finiscono.
Suggerimento AI: Pyright + Copilot/Claude diventa molto più utile una volta che il tuo codice è tipizzato: gli errori di tipo entrano nel contesto dell’AI e i suggerimenti diventano molto più accurati. Tipizza prima le tue interfacce, anche se gli interni restano debolmente tipizzati. I confini tra moduli e le API pubbliche delle tue classi sono dove il typing rende di più, sia per gli umani che per il modello seduto nel tuo editor.
Quando NON tipizzare
Il typing non è gratis. Aggiungere tipi a uno script usa-e-getta da 30 righe che cancellerai domani è uno spreco. In particolare:
- Esplorazione nel REPL. Nessuno tipizza nel REPL. Non cominciare.
- Script una tantum. Uno script da 50 righe che fa scraping di una pagina una volta non è dove il typing si ripaga.
- Notebook di colla. Notebook Jupyter per analisi esplorativa. Il ritmo è troppo veloce e il ciclo di vita troppo corto.
- Codice davvero dinamico. Alcuni pattern di metaprogrammazione sfidano davvero il typing statico. Quando ti ritrovi a combattere il sistema di tipi per mezz’ora, un commento
# type: ignoreva bene.
Cosa dovrebbe essere tipizzato: ogni codice che spedisci, ogni codice in src/, ogni funzione che qualcun altro potrebbe chiamare, ogni API pubblica. Vale la regola dell’80/20: tipizza i confini, le data class, le firme di funzione. Helper interni di una riga possono restare lassi.
Un esempio sviluppato
Tipizziamo una piccola funzione nel modo in cui la scriveresti davvero nel 2026.
from collections.abc import Iterable
from dataclasses import dataclass
from decimal import Decimal
@dataclass
class LineItem:
sku: str
quantity: int
unit_price: Decimal
def order_total(items: Iterable[LineItem], vat_rate: Decimal = Decimal("0.22")) -> Decimal:
"""Calcola il totale IVA inclusa per una sequenza di line item."""
subtotal = sum((item.unit_price * item.quantity for item in items), start=Decimal("0"))
return subtotal * (1 + vat_rate)
Tre cose che vale la pena notare:
Iterable[LineItem]nonlist[LineItem]. La funzione non ha bisogno di una lista; itera una sola volta. AccettareIterablepermette al chiamante di passare un generator, una tuple o una list. Sii liberale in ciò che accetti.Decimalnonfloatper i soldi. Sempre. I float sono per la scienza; i decimal sono per i soldi.pyrightti prenderà se li mescoli.- Il tipo di ritorno è esplicito. La funzione è abbastanza corta che l’inferenza avrebbe funzionato, ma scriverlo è un contratto per i chiamanti.
Riepilogo
I tipi non sono più opzionali nel Python moderno. Usa la sintassi nuova (list[int], int | None, def f[T](...)). Usa pyright o mypy. Tipizza al minimo i confini dei tuoi moduli, e il tuo assistente AI ti ricompenserà tipizzando di più. Non tipizzare il REPL o gli script usa-e-getta.
La lezione 3 (martedì) è su tre feature sintattiche che hanno cambiato il modo in cui Python da lavoro viene scritto: f-string, walrus operator e pattern matching. Scriveremo codice, non teoria.
Letture di approfondimento
- PEP 484 - Type Hints: l’originale.
- PEP 695 - Type Parameter Syntax: la revisione dei generics nella 3.12.
- Python typing docs: riferimento completo.
- pyright documentation: il checker su cui ci appoggeremo.