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:
# In una grande codebase, cosa si aspetta e cosa restituisce questa funzione?
def calculate_discount(customer, items, code):
# ... 50 righe di codice ...
return resultSenza 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:
# Senza type hints
def add(a, b):
return a + b
# Con type hints
def add(a: int, b: int) -> int:
return a + bLa 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:
def calculate_discount(customer: dict, items: list, code: str) -> float:
# ... 50 righe di codice ...
return resultOra è 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:
- Opzionale: non devi mai usare i type hints
- Graduale: puoi aggiungere hints ad alcune parti del tuo codice e non ad altre
- Non intrusivo: gli hints non cambiano il modo in cui Python esegue il tuo codice
- 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.
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:
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:
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 iterableIl 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:
- Documentazione: ti dicono quali tipi una funzione si aspetta e restituisce
- Supporto IDE: il tuo editor può usare gli hints per fornire autocompletamento e mostrare avvisi
- Analisi statica: strumenti esterni (come mypy) possono controllare il tuo codice per errori di tipo prima di eseguirlo
- 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:
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 strIl 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ì:
- Scrivi codice con i type hints
- Il tuo IDE mostra avvisi quando i tipi non corrispondono
- Risolvi i problemi prima di eseguire il codice
- 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.
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.
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:
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:
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.0Qui, 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:
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 DoeIl -> 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:
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: TrueSenza 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:
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.00Questa firma di funzione ti dice tutto ciò che ti serve sapere:
- Accetta due parametri
float:priceediscount_percent - Restituisce un valore
float - Non devi leggere l’implementazione per capire come usare questa funzione
Vediamo un altro esempio con tipi diversi:
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:
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:
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.995Ogni 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:
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
- 200L’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:
# 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 oldLa 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:
# 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.
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.0Senza 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:
# 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:
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:
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 foundIl 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:
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: NoneIl 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:
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 foundLa 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:
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: PrinceIl 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:
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:
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.74Il type hint list[float] significa "una lista che contiene float". Questo è più informativo di un semplice list.
Ecco un altro esempio con stringhe:
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, CharlieIl 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:
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: 92Il 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:
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 oldIl 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:
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: TrueIl 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 (...):
# 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:
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.7Il type hint dict[str, list[int]] significa "un dizionario con chiavi stringa e valori lista-di-interi".
Ecco un esempio più complesso:
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, PhysicsIl 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.