Python & AI Tutorials Logo
Programmazione Python

42. Introduzione graduale ai type hints (Opzionale)

Nel corso di questo libro, hai scritto codice Python senza specificare quali tipi di dati contengono le tue variabili o quali tipi le tue funzioni accettano e restituiscono. Python ha funzionato perfettamente anche così: è un linguaggio a tipizzazione dinamica (dynamically typed), il che significa che i tipi vengono determinati a runtime mentre il programma viene eseguito. Questa flessibilità è uno dei maggiori punti di forza di Python, perché ti permette di scrivere codice in modo rapido ed espressivo.

Tuttavia, man mano che i programmi diventano più grandi e complessi, questa flessibilità può talvolta rendere il codice più difficile da comprendere e mantenere. Quando vedi una funzione come def process_data(items):, potresti chiederti: Che tipo di dati contiene items? Una lista(list) di stringhe? Un dizionario? Qualcos’altro?

I type hints (chiamati anche type annotations) offrono un modo per documentare i tipi attesi nel tuo codice. Sono aggiunte opzionali a Python che possono rendere il tuo codice più chiaro, aiutare a individuare gli errori prima e abilitare potenti funzionalità dell’IDE, il tutto senza cambiare il modo in cui Python esegue realmente il tuo codice.

Questo capitolo introduce i type hints in modo graduale, mostrandoti cosa sono, perché esistono e come usarli in modo efficace. Poiché i type hints sono opzionali e non influenzano il modo in cui Python esegue il tuo codice, l’intero capitolo è contrassegnato come opzionale. Puoi scrivere ottimi programmi Python senza mai usare i type hints. Ma comprenderli ti aiuterà a leggere il codice Python moderno e a decidere quando potrebbero essere utili per i tuoi progetti.

42.1) Perché sono stati aggiunti i type hints a Python

Python è stato progettato fin dall’inizio come linguaggio a tipizzazione dinamica. Per decenni, i programmatori Python hanno scritto codice senza alcuna informazione sui tipi, e questo ha funzionato meravigliosamente per innumerevoli progetti. Perché allora i type hints sono stati aggiunti a Python nel 2015 (con Python 3.5)?

La sfida delle grandi codebase

Man mano che Python diventava più popolare per applicazioni su larga scala, i team hanno incontrato delle sfide:

python
# In una grande codebase, cosa si aspetta e cosa restituisce questa funzione?
def calculate_discount(customer, items, code):
    # ... 50 righe di codice ...
    return result

Senza leggere l’intero corpo della funzione o la sua documentazione, non puoi capire:

  • customer è un dizionario, un oggetto personalizzato o qualcos’altro?
  • items è una lista, una tupla o un set?
  • Che tipo è code: una stringa, un intero?
  • Cosa restituisce la funzione: un numero, un dizionario o magari None?

Nei programmi piccoli, questa ambiguità è gestibile. Puoi facilmente guardare come la funzione viene usata altrove. Ma in una codebase con migliaia di funzioni distribuite su decine di file, questo diventa difficile.

La soluzione: type hints opzionali

I creatori di Python hanno deciso di aggiungere un sistema opzionale per documentare i tipi. La parola chiave è "opzionale": i type hints sono completamente volontari. Puoi usarli quando ti aiutano, ignorarli quando non lo fanno, e mescolare liberamente codice annotato e non annotato.

Ecco un esempio semplice per mostrare la sintassi di base:

python
# Senza type hints
def add(a, b):
    return a + b
 
# Con type hints
def add(a: int, b: int) -> int:
    return a + b

La sintassi è semplice:

  • Due punti (:) dopo un parametro mostrano quale tipo dovrebbe essere: a: int
  • Freccia (->) prima dei due punti mostra quale tipo restituisce la funzione: -> int

Ora vediamo questo con l’esempio precedente:

python
def calculate_discount(customer: dict, items: list, code: str) -> float:
    # ... 50 righe di codice ...
    return result

Ora è immediatamente chiaro: customer è un dizionario, items è una lista, code è una stringa e la funzione restituisce un float.

Non preoccuparti se questa sintassi ti sembra poco familiare: la esploreremo nel dettaglio nelle sezioni 42.3-42.6. Per ora, nota solo come puoi capire a colpo d’occhio cosa la funzione si aspetta e cosa restituisce.

Con o senza type hints, la funzione funziona esattamente allo stesso modo: Python non controlla questi tipi a runtime. (Esploreremo questo punto importante nel dettaglio nella sezione 42.2)

Un approccio graduale e pragmatico

Il sistema di type hints di Python è stato progettato per essere:

  1. Opzionale: non devi mai usare i type hints
  2. Graduale: puoi aggiungere hints ad alcune parti del tuo codice e non ad altre
  3. Non intrusivo: gli hints non cambiano il modo in cui Python esegue il tuo codice
  4. Tool-friendly: strumenti esterni possono controllare gli hints, ma Python stesso li ignora a runtime

Questo approccio pragmatico permette a Python di rimanere flessibile fornendo al contempo benefici a chi li desidera.

No grazie

Sì, per favore

Codice Python

Aggiungere type hints?

✓ Funziona perfettamente
✓ Mantienilo semplice

Aggiungi type hints

✓ Stesso comportamento a runtime

✓ Migliore supporto IDE

✓ Individua errori prima

42.2) La regola d’oro: nessuna applicazione forzata a runtime

La cosa più importante da capire sui type hints è questa: Python non applica in modo forzato i type hints a runtime. Sono puramente informativi. Vediamo cosa significa in pratica questa sorprendente realtà.

I type hints non impediscono tipi errati

Considera questa funzione con type hints:

python
def greet(name: str) -> str:
    return f"Hello, {name}!"
 
# Questo funziona bene, anche se 42 non è una stringa
result = greet(42)
print(result)  # Output: Hello, 42!

Il type hint dice chiaramente che name dovrebbe essere una stringa, ma Python accetta felicemente l’intero 42 ed esegue la funzione. Python non controlla il type hint: usa semplicemente il valore che fornisci.

Questo è fondamentalmente diverso da linguaggi come Java o C++, in cui il compilatore controlla i tipi prima di eseguire il codice e si rifiuta di eseguire se c’è una mancata corrispondenza di tipo. L’approccio di Python è più permissivo: si fida che tu fornisca i tipi corretti, ma non ti costringe a farlo.

Il problema: i rischi della tipizzazione dinamica restano

Ecco la vera sfida: anche con i type hints, la tipizzazione dinamica di Python significa che puoi comunque commettere errori di tipo che compaiono solo a runtime:

python
def calculate_total(prices: list) -> float:
    """Calculate the sum of prices."""
    return sum(prices)
 
# Questo funziona bene
print(calculate_total([10.99, 5.50, 3.25]))  # Output: 19.74
 
# Ma questo fallisce a runtime!
print(calculate_total("not a list"))  # TypeError: 'str' object is not iterable

Il type hint dice chiaramente che prices dovrebbe essere una lista, ma Python non ti impedisce di passare una stringa. L’errore appare solo quando il codice viene effettivamente eseguito e prova a usare sum() sulla stringa.

Abbiamo aggiunto i type hints per individuare questi problemi, ma i rischi della tipizzazione dinamica sono ancora lì. Gli errori di tipo possono nascondersi nel tuo codice fino al runtime, potenzialmente comparendo in produzione quando un utente fa qualcosa di inaspettato.

Quindi, se i type hints non impediscono errori a runtime, qual è il senso di usarli?

Allora a cosa servono i type hints?

I type hints possono non cambiare il comportamento a runtime di Python, ma svolgono uno scopo cruciale: forniscono informazioni a persone e strumenti, non a Python stesso:

  1. Documentazione: ti dicono quali tipi una funzione si aspetta e restituisce
  2. Supporto IDE: il tuo editor può usare gli hints per fornire autocompletamento e mostrare avvisi
  3. Analisi statica: strumenti esterni (come mypy) possono controllare il tuo codice per errori di tipo prima di eseguirlo
  4. Comprensione del codice: rendono le grandi codebase più facili da leggere e mantenere

Pensa ai type hints come a commenti che gli strumenti possono capire. Non cambiano il modo in cui Python viene eseguito, ma ti aiutano a scrivere codice migliore.

Ma come ci aiuta questo, concretamente, a individuare quegli errori a runtime che abbiamo appena visto?

La soluzione: type hints + supporto IDE

È qui che i type hints brillano davvero. Anche se Python non li applica a runtime, il tuo IDE può individuare gli errori prima ancora che tu esegua il codice:

python
def add_numbers(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b
 
# Il tuo IDE mostrerà un avviso qui (prima di eseguire il codice)
result = add_numbers("Hello", "World")  # IDE: Warning - expected int, got str

Il tuo editor di codice vede i type hints e può avvisarti delle incompatibilità di tipo mentre scrivi, molto prima di eseguire il codice. Questo intercetta molti bug durante lo sviluppo invece che in produzione.

Lo sviluppo Python moderno tipicamente funziona così:

  1. Scrivi codice con i type hints
  2. Il tuo IDE mostra avvisi quando i tipi non corrispondono
  3. Risolvi i problemi prima di eseguire il codice
  4. Gli errori a runtime dovuti a incompatibilità di tipo diventano molto più rari

Il type hint non impedisce l’errore a runtime, ma il tuo IDE lo usa per impedirti di scrivere codice buggato in primo luogo!

Il meglio di entrambi i mondi

I type hints danno a Python il meglio di entrambi i mondi: intercettare la maggior parte degli errori in anticipo mantenendo la flessibilità:

Sicurezza in sviluppo: il tuo IDE e i type checker catturano la maggior parte degli errori di tipo durante lo sviluppo, così trovi i bug presto.

python
def process(data: list) -> list:
    return [x * 2 for x in data]
 
# Se passi accidentalmente una stringa:
process("hello")  # IDE warns: expected list, got str
# Lo correggi prima di eseguire il codice!

Flessibilità a runtime: Python esegue ancora codice con incompatibilità di tipo, cosa che può essere utile per prototipazione rapida o quando vuoi intenzionalmente accettare più tipi.

python
def add_numbers(a: int, b: int) -> int:
    return a + b
 
# Python eseguirà questo, anche se i tipi non corrispondono
print(add_numbers(5.5, 3.2))        # Output: 8.7 (works!)
print(add_numbers("Hi", " there"))  # Output: Hi there (also works!)

Questa flessibilità significa che non sei vincolato a un sistema di tipi rigido. Quando devi infrangere le regole (per test, prototipazione o casi d’uso legittimi), Python te lo permette. Ma quando scrivi codice di produzione, il tuo IDE ti mantiene al sicuro.

Ricorda la regola d’oro: i type hints non cambiano il comportamento a runtime di Python: forniscono solo a te e ai tuoi strumenti le informazioni necessarie per individuare i problemi in anticipo. Devi comunque fare attenzione, ma ora hai potenti alleati che ti guardano le spalle.

42.3) Annotare le funzioni: parametri e valori di ritorno

L’uso più comune dei type hints è annotare i parametri delle funzioni e i valori di ritorno. Questo dice ai lettori (e agli strumenti) quali tipi una funzione si aspetta e produce. Iniziamo dal caso più semplice e costruiamo gradualmente.

Le basi: annotazioni dei parametri

Per aggiungere un type hint a un parametro, metti due punti dopo il nome del parametro, seguiti dal tipo:

python
def greet(name: str):
    """Greet a person by name."""
    return f"Hello, {name}!"
 
# Utilizzo
message = greet("Alice")
print(message)  # Output: Hello, Alice!

La sintassi name: str significa "il parametro name dovrebbe essere una stringa". Puoi aggiungere type hints a più parametri:

python
def calculate_area(width: float, height: float):
    """Calculate the area of a rectangle."""
    return width * height
 
# Utilizzo
area = calculate_area(5.0, 3.0)
print(area)  # Output: 15.0

Qui, sia width sia height sono annotati come float. La funzione funziona allo stesso modo di prima: i type hints non cambiano il comportamento, ma ora il tuo IDE sa quali tipi aspettarsi.

Aggiungere annotazioni del tipo di ritorno

Per specificare quale tipo restituisce una funzione, aggiungi -> type dopo la lista dei parametri e prima dei due punti:

python
def get_full_name(first: str, last: str) -> str:
    """Combine first and last names."""
    return f"{first} {last}"
 
# Utilizzo
name = get_full_name("John", "Doe")
print(name)  # Output: John Doe

Il -> str significa "questa funzione restituisce una stringa". Le annotazioni del tipo di ritorno sono particolarmente utili quando il tipo di ritorno non è ovvio dal nome della funzione:

python
def is_adult(age: int) -> bool:
    """Check if someone is an adult (18 or older)."""
    return age >= 18
 
# Utilizzo
adult = is_adult(25)
print(adult)  # Output: True

Senza guardare l’implementazione, sai immediatamente che questa funzione restituisce un valore booleano.

Mettere insieme tutto: una funzione completa

La maggior parte delle funzioni avrà sia annotazioni dei parametri sia del tipo di ritorno. Ecco come appare una funzione completamente annotata:

python
def calculate_discount(price: float, discount_percent: float) -> float:
    """Calculate the discounted price."""
    discount_amount = price * (discount_percent / 100)
    return price - discount_amount
 
# Utilizzo
original_price = 100.0
discount = 20.0
final_price = calculate_discount(original_price, discount)
print(f"Final price: ${final_price:.2f}")  # Output: Final price: $80.00

Questa firma di funzione ti dice tutto ciò che ti serve sapere:

  • Accetta due parametri float: price e discount_percent
  • Restituisce un valore float
  • Non devi leggere l’implementazione per capire come usare questa funzione

Vediamo un altro esempio con tipi diversi:

python
def repeat_message(message: str, times: int) -> str:
    """Repeat a message a specified number of times."""
    return message * times
 
# Utilizzo
repeated = repeat_message("Hello! ", 3)
print(repeated)  # Output: Hello! Hello! Hello! 

I type hints rendono chiaro che passi una stringa e un intero, e ottieni indietro una stringa.

Lavorare con i valori predefiniti

Quando un parametro ha un valore predefinito, inserisci il type hint tra il nome del parametro e il valore predefinito:

python
def create_greeting(name: str, formal: bool = False) -> str:
    """Create a greeting message."""
    if formal:
        return f"Good day, {name}."
    return f"Hi, {name}!"
 
# Utilizzo
print(create_greeting("Alice"))              # Output: Hi, Alice!
print(create_greeting("Bob", formal=True))   # Output: Good day, Bob.

La sintassi formal: bool = False significa "formal è un booleano con valore predefinito False".

Puoi avere più parametri con valori predefiniti, tutti annotati:

python
def format_price(amount: float, currency: str = "USD", decimals: int = 2) -> str:
    """Format a price with currency symbol."""
    if currency == "USD":
        symbol = "$"
    elif currency == "EUR":
        symbol = "€"
    else:
        symbol = currency
    
    return f"{symbol}{amount:.{decimals}f}"
 
# Utilizzo
print(format_price(99.99))                          # Output: $99.99
print(format_price(99.99, "EUR"))                   # Output: €99.99
print(format_price(99.995, "USD", 3))               # Output: $99.995

Ogni parametro mostra chiaramente il suo tipo e il valore predefinito, rendendo la funzione facile da capire e usare.

Caso speciale: funzioni che non restituiscono valori

Alcune funzioni eseguono solo azioni (come stampare o scrivere su un file) senza restituire un valore. Per rendere chiaro che queste funzioni non restituiscono nulla, usa -> None:

python
def print_report(title: str, data: list) -> None:
    """Print a formatted report."""
    print(f"=== {title} ===")
    for item in data:
        print(f"  - {item}")
    # Nessuna istruzione return, quindi restituisce None implicitamente
 
# Utilizzo
print_report("Sales Data", [100, 150, 200])

Output:

=== Sales Data ===
  - 100
  - 150
  - 200

L’annotazione -> None indica esplicitamente che questa funzione non restituisce un valore significativo.

Perché usare -> None?

  • Chiarezza: rende esplicita la tua intenzione: questa funzione serve per azioni, non per risultati
  • Supporto IDE: il tuo IDE può avvisarti se provi accidentalmente a usare il valore di ritorno

42.4) Semplici annotazioni delle variabili

Anche se i type hints sono usati più comunemente con le funzioni, puoi anche annotare le variabili. Vediamo come funziona e quando è effettivamente utile.

Sintassi di base per l’annotazione delle variabili

Per annotare una variabile, usa la stessa sintassi con i due punti dei parametri di funzione:

python
# Annotare le variabili
name: str = "Alice"
age: int = 30
height: float = 5.7
is_student: bool = True
 
print(f"{name} is {age} years old")  # Output: Alice is 30 years old

La sintassi name: str = "Alice" significa "la variabile name è una stringa e ha il valore 'Alice'". L’annotazione non cambia il modo in cui la variabile funziona: è puramente informativa.

Le annotazioni delle variabili spesso vengono saltate

In pratica, le annotazioni delle variabili sono usate raramente. Il motivo è semplice: Python può dedurre il tipo dal valore, quindi le annotazioni sono di solito ridondanti:

python
# Queste annotazioni sono inutili
name: str = "Alice"  # Ovviamente una stringa
count: int = 0  # Ovviamente un int
prices: list = [10.99, 5.50]  # Ovviamente una list
settings: dict = {}  # Ovviamente un dict
 
# Scrivi semplicemente questo invece
name = "Alice"
count = 0
prices = [10.99, 5.50]
settings = {}

Quando scrivi name = "Alice", sia tu sia il tuo IDE sapete immediatamente che è una stringa. L’annotazione non aggiunge alcuna informazione utile.

Nel codice Python del mondo reale, vedrai raramente annotazioni di variabili. È normale e previsto. Le annotazioni delle funzioni sono molto più importanti e comuni.

L’unico caso utile: dichiarare variabili prima dell’assegnazione

C’è una situazione in cui le annotazioni delle variabili sono davvero utili: quando devi dichiarare una variabile prima di assegnarle un valore.

python
def calculate_statistics(numbers: list) -> dict:
    """Calculate basic statistics from a list of numbers."""
    # Dichiara le variabili prima di usarle
    total: float
    count: int
    average: float
    
    # Ora assegna i valori
    total = sum(numbers)
    count = len(numbers)
    average = total / count if count > 0 else 0.0
    
    return {
        "total": total,
        "count": count,
        "average": average
    }
 
# Utilizzo
result = calculate_statistics([10, 20, 30, 40])
print(f"Average: {result['average']}")  # Output: Average: 25.0

Senza annotazioni, non puoi dichiarare una variabile senza anche assegnarle un valore. Le annotazioni ti permettono di specificare i tipi in anticipo, il che può rendere più chiara la struttura del codice.

Questo è il principale caso d’uso pratico delle annotazioni di variabili.

Ricorda: le variabili possono essere riassegnate a tipi diversi

Anche con un’annotazione di tipo, puoi riassegnare una variabile a un tipo diverso:

python
# Inizia con una stringa
value: str = "hello"
print(value)  # Output: hello
 
# Riassegnazione a un tipo diverso - Python lo consente
value = 42
print(value)  # Output: 42
 
# Un altro cambio di tipo - ancora consentito
value = [1, 2, 3]
print(value)  # Output: [1, 2, 3]

Il tuo IDE o un type checker statico ti avviserà di questi cambi di tipo, ma Python stesso non li impedisce. I type hints ti guidano verso la coerenza, ma non la applicano a runtime.

42.5) Gestire "None": tipi optional e l’operatore |

Uno dei pattern più comuni in Python è una funzione che potrebbe restituire un valore oppure potrebbe restituire None. Per esempio, cercare un elemento potrebbe andare a buon fine (restituendo l’elemento) oppure fallire (restituendo None). I type hints offrono modi chiari per esprimere questo pattern.

Il problema: funzioni che potrebbero restituire None

Considera questa funzione che cerca un utente:

python
def find_user_by_email(email: str) -> dict:
    """Find a user by email address."""
    users = [
        {"name": "Alice", "email": "alice@example.com"},
        {"name": "Bob", "email": "bob@example.com"}
    ]
    
    for user in users:
        if user["email"] == email:
            return user
    
    return None  # Type mismatch! This contradicts the -> dict hint
 
# Utilizzo
user = find_user_by_email("alice@example.com")
if user:
    print(f"Found: {user['name']}")  # Output: Found: Alice
else:
    print("User not found")

Il type hint -> dict è fuorviante perché la funzione può restituire None. Un type checker statico ti avviserebbe che restituire None non corrisponde al tipo di ritorno dichiarato dict.

Soluzione: usare l’operatore | per i tipi optional

Python 3.10 ha introdotto l’operatore | per i type hints, che significa "oppure". Puoi usarlo per indicare che una funzione potrebbe restituire un tipo o un altro:

python
def find_user_by_email(email: str) -> dict | None:
    """Find a user by email address. Returns None if not found."""
    users = [
        {"name": "Alice", "email": "alice@example.com"},
        {"name": "Bob", "email": "bob@example.com"}
    ]
    
    for user in users:
        if user["email"] == email:
            return user
    
    return None
 
# Utilizzo
user = find_user_by_email("alice@example.com")
if user:
    print(f"Found: {user['name']}")  # Output: Found: Alice
 
missing = find_user_by_email("charlie@example.com")
if missing is None:
    print("User not found")  # Output: User not found

Il type hint -> dict | None significa "questa funzione restituisce o un dizionario o None". Questo descrive accuratamente il comportamento della funzione.

Nota: nel codice Python più vecchio (prima della 3.10), potresti vedere Optional[dict] dal modulo typing invece di dict | None. Significano la stessa cosa, ma | è la sintassi moderna e preferita.

Usare | con più tipi

Puoi usare | per indicare più di due tipi possibili:

python
def parse_value(text: str) -> int | float | None:
    """Parse a string into a number. Returns None if parsing fails."""
    try:
        # Prova prima a fare il parsing come intero
        if '.' not in text:
            return int(text)
        # Altrimenti fai il parsing come float
        return float(text)
    except ValueError:
        return None
 
# Utilizzo
print(parse_value("42"))      # Output: 42 (int)
print(parse_value("3.14"))    # Output: 3.14 (float)
print(parse_value("invalid")) # Output: None

Il type hint -> int | float | None significa che la funzione può restituire un intero, un float o None.

Controllare None: best practice

Quando una funzione può restituire None, controlla sempre None prima di usare il risultato. Altrimenti, rischi errori quando provi a usare None come se fosse il tipo atteso:

python
def get_user_age(user_id: int) -> int | None:
    """Get user's age. Returns None if user not found."""
    users = {1: 25, 2: 30, 3: 35}
    return users.get(user_id)
 
# Controlla sempre None prima di usare il valore
age = get_user_age(1)
if age is not None:
    print(f"User is {age} years old")  # Output: User is 25 years old
    if age >= 18:
        print("User is an adult")  # Output: User is an adult
else:
    print("User not found")
 
# Per utenti inesistenti
age = get_user_age(999)
if age is None:
    print("User not found")  # Output: User not found

La chiave è usare if age is not None: o if age is None: per controllare esplicitamente prima di usare il valore.

Parametri opzionali con | None

Puoi usare | anche con i parametri, spesso combinandolo con valori predefiniti:

python
def format_name(first: str, middle: str | None = None, last: str = "") -> str:
    """Format a full name. Middle name is optional."""
    if middle and last:
        return f"{first} {middle} {last}"
    elif last:
        return f"{first} {last}"
    return first
 
# Utilizzo
print(format_name("John", "Q", "Doe"))    # Output: John Q Doe
print(format_name("Jane", None, "Smith")) # Output: Jane Smith
print(format_name("Prince"))              # Output: Prince

Il type hint middle: str | None = None indica che middle può essere una stringa o None, con None come valore predefinito. Questo è un pattern comune per parametri opzionali.

42.6) Leggere i type hints comuni: list, dict, tuple

Man mano che leggi codice Python scritto da altri, incontrerai type hints per collezioni come liste, dizionari e tuple. Python moderno offre modi chiari per specificare non solo che qualcosa è una lista, ma anche che tipo di elementi contiene la lista.

Nota: la sintassi mostrata qui (list[int], dict[str, int], ecc.) funziona in Python 3.9+. Nel codice più vecchio, potresti vedere List[int] e Dict[str, int] (con la maiuscola) dal modulo typing: funzionano allo stesso modo.

Type hints di base per le collezioni

I type hints più semplici per le collezioni specificano solo il tipo di collezione:

python
def print_items(items: list) -> None:
    """Print all items in a list."""
    for item in items:
        print(item)
 
def get_user_settings() -> dict:
    """Get user settings as a dictionary."""
    return {"theme": "dark", "notifications": True}
 
def get_position() -> tuple:
    """Get x, y position."""
    return (10, 20)

Questi hints ti dicono il tipo di collezione ma non cosa c’è dentro.

Liste: specificare i tipi degli elementi

Per specificare che tipo di elementi contiene una lista, usa le parentesi quadre:

python
def calculate_total(prices: list[float]) -> float:
    """Calculate the total of all prices."""
    return sum(prices)
 
# Utilizzo
total = calculate_total([10.99, 5.50, 3.25])
print(f"Total: ${total:.2f}")  # Output: Total: $19.74

Il type hint list[float] significa "una lista che contiene float". Questo è più informativo di un semplice list.

Ecco un altro esempio con stringhe:

python
def format_names(names: list[str]) -> str:
    """Format a list of names as a comma-separated string."""
    return ", ".join(names)
 
# Utilizzo
students = ["Alice", "Bob", "Charlie"]
print(format_names(students))  # Output: Alice, Bob, Charlie

Il type hint list[str] significa "una lista che contiene stringhe".

Dizionari: specificare i tipi di chiavi e valori

Per i dizionari, specifica sia il tipo della chiave sia il tipo del valore:

python
def get_student_grades() -> dict[str, int]:
    """Get student names mapped to their grades."""
    return {
        "Alice": 95,
        "Bob": 87,
        "Charlie": 92
    }
 
# Utilizzo
grades = get_student_grades()
for name, grade in grades.items():
    print(f"{name}: {grade}")

Output:

Alice: 95
Bob: 87
Charlie: 92

Il type hint dict[str, int] significa "un dizionario con chiavi stringa e valori interi".

Ecco un esempio in cui i valori possono essere di più tipi:

python
def get_user_data(user_id: int) -> dict[str, str | int]:
    """Get user data. Values can be strings or integers."""
    return {
        "name": "Alice",
        "email": "alice@example.com",
        "age": 30,
        "id": 12345
    }
 
# Utilizzo
user = get_user_data(1)
print(f"{user['name']} is {user['age']} years old")  # Output: Alice is 30 years old

Il type hint dict[str, str | int] significa "un dizionario con chiavi stringa e valori che sono o stringhe o interi".

Tuple: lunghezza fissa e variabile

Le tuple sono diverse dalle liste perché spesso hanno una struttura fissa. Puoi specificare il tipo di ogni posizione:

python
def get_user_info(user_id: int) -> tuple[str, int, bool]:
    """
    Get user information as a tuple.
    Returns: (name, age, is_active)
    """
    return ("Alice", 30, True)
 
# Utilizzo
name, age, active = get_user_info(1)
print(f"{name}, {age}, active: {active}")  # Output: Alice, 30, active: True

Il type hint tuple[str, int, bool] significa "una tupla con esattamente tre elementi: una stringa, un intero e un booleano, in quest’ordine".

Per tuple di lunghezza variabile con elementi dello stesso tipo, usa i puntini di sospensione (...):

python
# Tupla a lunghezza fissa: esattamente 2 float
def get_2d_point() -> tuple[float, float]:
    """Get 2D coordinates (x, y)."""
    return (10.5, 20.3)
 
# Tupla a lunghezza variabile: qualsiasi numero di float
def get_coordinates() -> tuple[float, ...]:
    """Get coordinates. Can be 2D, 3D, or any dimension."""
    return (10.5, 20.3, 15.7)  # 3D in this case
 
# Utilizzo
point = get_2d_point()
coords = get_coordinates()
print(f"2D point: {point}")       # Output: 2D point: (10.5, 20.3)
print(f"Coordinates: {coords}")   # Output: Coordinates: (10.5, 20.3, 15.7)

Il type hint tuple[float, ...] significa "una tupla che contiene un numero qualsiasi di float". Il ... significa "un numero qualsiasi di questo tipo".

Collezioni annidate

Puoi annidare i type hints per strutture dati complesse. Iniziamo con un esempio semplice:

python
def get_scores_by_student() -> dict[str, list[int]]:
    """Get test scores for each student."""
    return {
        "Alice": [95, 87, 92],
        "Bob": [88, 91, 85],
        "Charlie": [90, 88, 94]
    }
 
# Utilizzo
scores = get_scores_by_student()
for name, tests in scores.items():
    average = sum(tests) / len(tests)
    print(f"{name}: {average:.1f}")

Output:

Alice: 91.3
Bob: 88.0
Charlie: 90.7

Il type hint dict[str, list[int]] significa "un dizionario con chiavi stringa e valori lista-di-interi".

Ecco un esempio più complesso:

python
def get_student_records() -> list[dict[str, str | int]]:
    """Get a list of student records."""
    return [
        {"name": "Alice", "age": 20, "major": "CS"},
        {"name": "Bob", "age": 21, "major": "Math"},
        {"name": "Charlie", "age": 19, "major": "Physics"}
    ]
 
# Utilizzo
students = get_student_records()
for student in students:
    print(f"{student['name']}, {student['age']}, {student['major']}")

Output:

Alice, 20, CS
Bob, 21, Math
Charlie, 19, Physics

Il type hint list[dict[str, str | int]] significa "una lista di dizionari, dove ogni dizionario ha chiavi stringa e valori che sono o stringhe o interi".

Leggere i type hints: un riferimento rapido

Quando incontri type hints nel codice, ecco come leggerli:

Collezioni:

  • list[int] - "una lista di interi"
  • dict[str, float] - "un dizionario con chiavi stringa e valori float"
  • tuple[str, int] - "una tupla con esattamente due elementi: una stringa, poi un intero"
  • tuple[float, ...] - "una tupla che contiene un numero qualsiasi di float"

Optional e tipi multipli:

  • int | None - "un intero o None"
  • str | int | float - "una stringa, un intero o un float"

Annidati:

  • list[dict[str, int]] - "una lista di dizionari (ogni dict ha chiavi stringa e valori interi)"
  • dict[str, list[float]] - "un dizionario con chiavi stringa e valori lista-di-float"

Nota: nel codice più vecchio (Python < 3.10), potresti vedere Union[int, str] invece di int | str, o Optional[int] invece di int | None. Significano la stessa cosa.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai