Type Hints in Python
Impara i type hints in Python: annotare variabili, funzioni e classi, usare il modulo typing e verificare i tipi con mypy.
I type hints ti permettono di associare informazioni sul tipo atteso a variabili, parametri di funzione e valori di ritorno. Python non li applica a runtime — sono metadati utilizzati da editor, linter e type-checker come mypy per rilevare bug prima ancora di eseguire una singola riga.
Questo capitolo tratta:
- Perché i type hints sono importanti e quando usarli
- Annotare variabili e funzioni
- Tipi built-in e il modulo
typing(List,Dict,Optional,Union,Tuple,Any,Callable) - Sintassi moderna (Python 3.10+)
- Annotare classi e
self - Generici e alias di tipo
- Analisi statica con mypy
- Errori comuni
Perché usare i Type Hints?
Python è tipizzato dinamicamente: una variabile può contenere qualsiasi valore di qualsiasi tipo. Questa flessibilità è potente, ma rende più difficile navigare i codebase di grandi dimensioni — non è possibile conoscere il tipo dell'argomento di una funzione solo leggendo il punto di chiamata.
I type hints risolvono questo problema senza rinunciare al dinamismo di Python:
- Gli editor segnalano gli errori immediatamente. VS Code, PyCharm e altri sottolineano le discrepanze di tipo mentre scrivi.
- Il refactoring diventa più sicuro. Cambia la firma di una funzione e il type-checker ti indica ogni punto di chiamata che si rompe.
- Il codice si documenta da solo.
def greet(name: str) -> strcomunica il contratto senza bisogno di una docstring. - Le librerie diventano più facili da usare. Le librerie tipizzate espongono il completamento automatico per ogni attributo e metodo.
I type hints sono stati introdotti in Python 3.5 tramite PEP 484. La sintassi è stata perfezionata in ogni versione principale da allora. Gli esempi riportati indicano la versione minima di Python in cui la sintassi è diventata disponibile.
Annotare Variabili
Aggiungi i due punti dopo il nome della variabile seguiti dal tipo:
name: str = "Alice"
age: int = 30
price: float = 9.99
is_active: bool = TruePuoi anche dichiarare il tipo di una variabile senza assegnarle ancora un valore. Questo si chiama dichiarazione anticipata ed è utile all'interno di classi o a livello di modulo:
user_id: int # declared but not yet assigned
user_id = 42Le annotazioni di tipo sulle variabili a livello di modulo non influenzano il comportamento a runtime — vengono memorizzate nel dizionario __annotations__ del modulo ma per il resto sono ignorate dall'interprete.
Annotare Funzioni
Inserisci le annotazioni sui parametri (dopo i due punti) e sul valore di ritorno (dopo -> prima dei due punti che terminano la firma):
def add(a: int, b: int) -> int:
return a + b
def greet(name: str) -> str:
return f"Hello, {name}!"
def send_email(to: str, subject: str, body: str) -> None:
print(f"Sending '{subject}' to {to}")
result: int = add(3, 5)
message: str = greet("Alice")-> None significa che la funzione non ha un valore di ritorno significativo (restituisce None implicitamente). Omettere l'annotazione di ritorno è valido, ma -> None esplicito rende chiara l'intenzione.
Parametri Predefiniti
I valori predefiniti vanno dopo l'annotazione:
def connect(host: str, port: int = 8080, secure: bool = False) -> None:
print(f"Connecting to {host}:{port} (secure={secure})")
connect("example.com") # uses defaults
connect("example.com", 443, True)*args e **kwargs
Annota il tipo dell'elemento, non il tipo della collezione:
def total(*prices: float) -> float:
return sum(prices)
def create_user(**fields: str) -> dict:
return fields
print(round(total(9.99, 4.50, 12.00), 2)) # 26.49
print(create_user(name="Bob", role="admin"))*prices: float significa che ogni argomento posizionale è un float; a runtime prices è comunque una tuple regolare di float. Allo stesso modo, **fields: str significa che il valore di ogni argomento keyword è una str.
Il Modulo typing
Per qualsiasi cosa oltre i tipi built-in di base, importa dal modulo typing (Python 3.5+). A partire da Python 3.9, molti tipi di typing sono stati integrati direttamente nei corrispondenti built-in (vedi Sintassi Moderna di seguito).
List, Tuple, Set, Dict
from typing import List, Tuple, Set, Dict
def first_names(users: List[str]) -> str:
return users[0] if users else ""
def dimensions() -> Tuple[int, int, int]:
return (1920, 1080, 32)
def unique_tags(items: List[str]) -> Set[str]:
return set(items)
def word_count(text: str) -> Dict[str, int]:
counts: Dict[str, int] = {}
for word in text.split():
counts[word] = counts.get(word, 0) + 1
return counts
print(first_names(["Alice", "Bob"])) # Alice
print(dimensions()) # (1920, 1080, 32)
print(unique_tags(["py", "web", "py"])) # {'py', 'web'}
print(word_count("one two one")) # {'one': 2, 'two': 1}Optional
Optional[X] è un'abbreviazione di Union[X, None]. Usalo ogni volta che un valore può essere assente:
from typing import Optional
def find_user(user_id: int) -> Optional[str]:
db = {1: "Alice", 2: "Bob"}
return db.get(user_id) # returns None if not found
name = find_user(1)
if name is not None:
print(name.upper()) # ALICE
missing = find_user(99)
print(missing) # NoneUn type-checker vede Optional[str] e sa che devi verificare None prima di chiamare metodi string sul risultato. Senza il controllo, segnala un errore.
Union
Union[X, Y] significa che il valore può essere di tipo X o di tipo Y:
from typing import Union
def stringify(value: Union[int, float, str]) -> str:
return str(value)
print(stringify(42)) # 42
print(stringify(3.14)) # 3.14
print(stringify("hi")) # hiUnion è più utile quando una funzione accetta genuinamente più tipi non correlati. Se ti trovi a scrivere Union[str, None], usa invece Optional[str] — è più idiomatico.
Callable
Callable[[ArgTypes...], ReturnType] annota una funzione passata come argomento:
from typing import Callable
def apply_twice(func: Callable[[int], int], value: int) -> int:
return func(func(value))
def double(n: int) -> int:
return n * 2
print(apply_twice(double, 3)) # 12Callable[[int], int] significa: un callable che accetta un argomento int e restituisce un int. Se la lista degli argomenti è complessa o sconosciuta, usa Callable[..., ReturnType].
Any
Any è un tipo speciale che disabilita il type-checking per quel valore. Ogni tipo è sia assegnabile a Any che assegnabile da Any:
from typing import Any
def log(value: Any) -> None:
print(value)
log(42)
log("hello")
log([1, 2, 3])Usa Any con parsimonia — è una via di fuga che rimuove la stessa protezione offerta dai type hints. È appropriato quando si interagisce con codice di terze parti non tipizzato, o durante una migrazione graduale di un codebase di grandi dimensioni.
Sintassi Moderna (Python 3.9+, 3.10+)
Generici built-in (Python 3.9+)
Da Python 3.9 in poi puoi usare i tipi built-in direttamente come generici, senza importare da typing:
# Python 3.9+
def word_count(text: str) -> dict[str, int]:
counts: dict[str, int] = {}
for word in text.split():
counts[word] = counts.get(word, 0) + 1
return counts
def first(items: list[int]) -> int | None:
return items[0] if items else None
print(word_count("cat dog cat")) # {'cat': 2, 'dog': 1}
print(first([10, 20, 30])) # 10
print(first([])) # NoneUsa list[str] invece di List[str], dict[str, int] invece di Dict[str, int], e così via.
Sintassi union X | Y (Python 3.10+)
Python 3.10 ha introdotto l'operatore | per le union, sostituendo Union[X, Y] e Optional[X]:
# Python 3.10+
def parse(value: str | int | None) -> str:
if value is None:
return "nothing"
return str(value)
print(parse("hello")) # hello
print(parse(42)) # 42
print(parse(None)) # nothingstr | None è equivalente a Optional[str]. Questa sintassi è più pulita e più facile da leggere.
Annotare Classi
Annota gli attributi di istanza all'interno di __init__ e aggiungi annotazioni di ritorno ai metodi:
class BankAccount:
owner: str # class-level annotation (no default value)
balance: float
def __init__(self, owner: str, initial_balance: float = 0.0) -> None:
self.owner = owner
self.balance = initial_balance
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("Deposit amount must be positive.")
self.balance += amount
def withdraw(self, amount: float) -> bool:
if amount > self.balance:
return False
self.balance -= amount
return True
def __repr__(self) -> str:
return f"BankAccount(owner={self.owner!r}, balance={self.balance:.2f})"
account = BankAccount("Alice", 100.0)
account.deposit(50.0)
print(account.withdraw(30.0)) # True
print(account) # BankAccount(owner='Alice', balance=120.00)L'annotazione su self è sempre inferita — non si scrive mai self: BankAccount. Il tipo di ritorno di __init__ è sempre None.
ClassVar
Usa ClassVar[T] (da typing) per contrassegnare un attributo che appartiene alla classe, non a ciascuna istanza:
from typing import ClassVar
class Config:
MAX_RETRIES: ClassVar[int] = 3
timeout: int
def __init__(self, timeout: int) -> None:
self.timeout = timeout
print(Config.MAX_RETRIES) # 3Un type-checker avverte se tenti di impostare ClassVar su un'istanza — è pensato per essere condiviso a livello di classe.
Alias di Tipo
Un alias di tipo assegna a un tipo lungo o complesso un nome più breve e significativo:
from typing import List, Tuple
# Simple alias
UserID = int
Filename = str
# Structured alias
Coordinates = Tuple[float, float]
Matrix = List[List[float]]
def distance(p1: Coordinates, p2: Coordinates) -> float:
return ((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) ** 0.5
print(distance((0.0, 0.0), (3.0, 4.0))) # 5.0Da Python 3.12, usa l'istruzione type per alias espliciti e ispezionabili:
# Python 3.12+
type Vector = list[float]
type Matrix = list[Vector]Generici con TypeVar
TypeVar ti permette di scrivere una singola funzione che funziona con qualsiasi tipo preservando comunque le relazioni tra i tipi:
from typing import TypeVar, List
T = TypeVar("T")
def first_item(items: List[T]) -> T:
return items[0]
x: int = first_item([1, 2, 3]) # x is int
s: str = first_item(["a", "b"]) # s is strIl type-checker deduce dall'argomento cosa sia T e porta tale informazione fino al tipo di ritorno. Senza TypeVar, dovresti restituire Any e perdere la sicurezza dei tipi.
Puoi vincolare TypeVar a un insieme di tipi consentiti:
from typing import TypeVar
Numeric = TypeVar("Numeric", int, float)
def double(n: Numeric) -> Numeric:
return n * 2
print(double(4)) # 8 (int)
print(double(2.5)) # 5.0 (float)Analisi Statica dei Tipi con mypy
mypy è il type-checker statico più diffuso per Python. Installalo con pip:
pip install mypyPoi eseguilo su un file:
mypy my_script.pyEsempio: rilevare un bug con mypy
Salva il seguente codice come demo.py:
def greet(name: str) -> str:
return f"Hello, {name}!"
result = greet(42) # passing int instead of str
print(result.upper())Eseguendo mypy demo.py viene riportato:
demo.py:4: error: Argument 1 to "greet" has incompatible type "int"; expected "str"
Found 1 error in 1 file (checked 1 source file)Python stesso esegue il codice senza problemi (le f-string convertono qualsiasi tipo), ma mypy ha rilevato la discrepanza prima che tu dovessi scoprirla in produzione.
Opzioni utili di mypy
| Flag | Effetto |
|---|---|
--strict | Abilita tutti i controlli opzionali (consigliato per nuovi progetti) |
--ignore-missing-imports | Sopprime gli errori riguardanti stub di terze parti mancanti |
--check-untyped-defs | Verifica anche le funzioni prive di annotazioni |
--disallow-untyped-defs | Richiede annotazioni su tutte le definizioni di funzione |
Un file mypy.ini (o [tool.mypy] in pyproject.toml) mantiene la configurazione fuori dalla riga di comando:
[mypy]
strict = true
ignore_missing_imports = trueTipizzazione Graduale
Non è necessario annotare ogni funzione in una volta sola. Python supporta la tipizzazione graduale: il codice annotato e quello non annotato coesistono senza problemi. mypy salta le funzioni non annotate per impostazione predefinita (a meno che non sia impostato --check-untyped-defs).
Un approccio pratico per un codebase esistente:
- Aggiungi annotazioni al nuovo codice fin dal primo giorno.
- Annota prima le funzioni più chiamate o più soggette a errori.
- Abilita
--strictmodulo per modulo man mano che la copertura migliora. - Usa
Anysolo dove una libreria di terze parti non è tipizzata, e aggiungi un commento che spieghi il perché.
Errori Comuni
Riferimenti anticipati
Se un tipo fa riferimento a una classe definita più avanti nello stesso file, racchiudi il nome tra virgolette per renderlo una stringa (un riferimento anticipato):
class Node:
def __init__(self, value: int, next: "Node | None" = None) -> None:
self.value = value
self.next = next
head = Node(1, Node(2))
print(head.value, head.next.value) # 1 2Da Python 3.10+, aggiungi invece from __future__ import annotations all'inizio del file. Questo rende tutte le annotazioni stringhe pigre ed elimina la necessità di virgolettare manualmente.
Annotazioni a runtime
Per impostazione predefinita, le annotazioni in Python 3.9 e versioni precedenti vengono valutate eagerly. Ciò significa che un riferimento anticipato senza virgolette genera un NameError:
# Works (with quotes):
def clone(self: "MyClass") -> "MyClass": ...Con from __future__ import annotations (Python 3.7+), tutte le annotazioni vengono memorizzate come stringhe e valutate solo quando ispezionate — risolvendo automaticamente il problema dei riferimenti anticipati.
None vs Optional
Un errore comune è annotare un tipo di ritorno come str quando la funzione può effettivamente restituire None. Usa sempre Optional[str] (o str | None) quando None è un possibile valore di ritorno:
from typing import Optional
# Wrong — mypy will flag callers that assume this is always str
def get_name(user_id: int) -> str:
if user_id == 0:
return None # type: ignore — this is the bug
# Correct
def get_name_safe(user_id: int) -> Optional[str]:
if user_id == 0:
return None
return "Alice"list vs List (compatibilità delle versioni)
Se il tuo codice viene eseguito su Python 3.8 o versioni precedenti, devi usare from typing import List e scrivere List[str]. Su Python 3.9+, list[str] funziona direttamente. Se devi supportare entrambi, usa le importazioni da typing oppure aggiungi from __future__ import annotations.
Riferimento Rapido
| Annotazione | Significato |
|---|---|
x: int | La variabile x è un intero |
def f(a: str) -> bool | Il parametro a è str; il valore di ritorno è bool |
-> None | La funzione non restituisce nulla di significativo |
Optional[str] | str o None |
Union[int, str] | int o str |
list[int] / List[int] | Lista di interi |
dict[str, int] / Dict[str, int] | Dict che mappa str a int |
tuple[int, str] / Tuple[int, str] | Tupla di (int, str) |
Callable[[int], str] | Funzione che accetta int, restituisce str |
Any | Qualsiasi tipo (disabilita il controllo) |
ClassVar[T] | Attributo a livello di classe |
TypeVar("T") | Variabile di tipo generico |
Argomenti Correlati
- Funzioni Python — dove vivono le annotazioni di tipo su parametri e valori di ritorno.
- Classi e Oggetti Python — per annotare
__init__, metodi e attributi di classe. - Dataclass Python — le annotazioni di tipo sono richieste per dichiarare i campi delle dataclass.
- Classi Astratte Python — le classi base astratte funzionano naturalmente con i type hints.