W3docs

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) -> str comunica 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 = True

Puoi 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 = 42

Le 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)               # None

Un 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"))    # hi

Union è 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))   # 12

Callable[[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([]))                   # None

Usa 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))      # nothing

str | 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)   # 3

Un 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.0

Da 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 str

Il 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 mypy

Poi eseguilo su un file:

mypy my_script.py

Esempio: 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

FlagEffetto
--strictAbilita tutti i controlli opzionali (consigliato per nuovi progetti)
--ignore-missing-importsSopprime gli errori riguardanti stub di terze parti mancanti
--check-untyped-defsVerifica anche le funzioni prive di annotazioni
--disallow-untyped-defsRichiede 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 = true

Tipizzazione 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:

  1. Aggiungi annotazioni al nuovo codice fin dal primo giorno.
  2. Annota prima le funzioni più chiamate o più soggette a errori.
  3. Abilita --strict modulo per modulo man mano che la copertura migliora.
  4. Usa Any solo 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 2

Da 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

AnnotazioneSignificato
x: intLa variabile x è un intero
def f(a: str) -> boolIl parametro a è str; il valore di ritorno è bool
-> NoneLa 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
AnyQualsiasi tipo (disabilita il controllo)
ClassVar[T]Attributo a livello di classe
TypeVar("T")Variabile di tipo generico

Argomenti Correlati

Esercitati

Pratica
What does Optional[str] mean in a Python type hint?
What does Optional[str] mean in a Python type hint?
Pratica
Which annotation correctly types a function that accepts a list of integers and returns a single integer?
Which annotation correctly types a function that accepts a list of integers and returns a single integer?
Pratica
What is the purpose of TypeVar in the typing module?
What is the purpose of TypeVar in the typing module?
Was this page helpful?