Python: caratteristiche moderne
Python ha subito una trasformazione significativa nel corso delle ultime versioni, introducendo funzionalità che lo rendono un linguaggio sempre più espressivo, sicuro e performante. Dalla tipizzazione statica opzionale ai pattern matching, dalle dataclass ai gestori di contesto asincroni, il Python moderno offre strumenti che permettono di scrivere codice più conciso, leggibile e manutenibile. In questo articolo esploreremo le caratteristiche più rilevanti introdotte a partire da Python 3.8 fino alle versioni più recenti.
Type hints e tipizzazione avanzata
I type hints, introdotti inizialmente in Python 3.5, hanno raggiunto una maturità notevole nelle versioni successive. A partire da Python 3.10 è possibile utilizzare l'operatore | per le union types, eliminando la necessità di importare Union dal modulo typing. Inoltre, i tipi built-in come list, dict e tuple possono essere usati direttamente come tipi generici senza ricorrere ai corrispettivi del modulo typing.
# Sintassi moderna per le union types (Python 3.10+)
def process_input(value: int | str | None) -> str:
if value is None:
return "nessun valore"
return str(value)
# Tipi generici built-in (Python 3.9+)
def filter_items(items: list[dict[str, int]]) -> list[str]:
# Filtra e restituisce solo le chiavi con valore positivo
result: list[str] = []
for item in items:
for key, val in item.items():
if val > 0:
result.append(key)
return result
Con Python 3.12 sono stati introdotti i type parameter syntax, che permettono di definire funzioni e classi generiche con una sintassi nativa molto più pulita rispetto all'uso di TypeVar.
# Sintassi generica moderna con type parameter (Python 3.12+)
def first_element[T](items: list[T]) -> T | None:
# Restituisce il primo elemento o None se la lista è vuota
return items[0] if items else None
class Stack[T]:
"""Implementazione di uno stack generico."""
def __init__(self) -> None:
# Inizializza lo stack interno
self._items: list[T] = []
def push(self, item: T) -> None:
# Aggiunge un elemento in cima allo stack
self._items.append(item)
def pop(self) -> T:
# Rimuove e restituisce l'elemento in cima
if not self._items:
raise IndexError("lo stack è vuoto")
return self._items.pop()
def peek(self) -> T | None:
# Restituisce l'elemento in cima senza rimuoverlo
return self._items[-1] if self._items else None
Il modulo typing offre inoltre costrutti avanzati come TypeAlias, TypeGuard e TypedDict che consentono di esprimere vincoli di tipo complessi in modo chiaro e verificabile da strumenti come mypy e pyright.
from typing import TypedDict, TypeGuard
class UserProfile(TypedDict):
name: str
age: int
email: str
active: bool
def is_valid_profile(data: dict) -> TypeGuard[UserProfile]:
# Verifica che il dizionario contenga tutti i campi richiesti
required_keys = {"name", "age", "email", "active"}
return required_keys.issubset(data.keys())
def greet_user(data: dict) -> str:
if is_valid_profile(data):
# Qui il type checker riconosce data come UserProfile
return f"Benvenuto, {data['name']}!"
return "Profilo non valido"
Pattern matching strutturale
Introdotto in Python 3.10, il pattern matching strutturale tramite l'istruzione match/case rappresenta una delle aggiunte più significative al linguaggio. A differenza di un semplice costrutto switch/case, il pattern matching di Python è in grado di destrutturare oggetti complessi, verificare tipi e catturare valori in un'unica espressione.
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
@dataclass
class Circle:
center: Point
radius: float
@dataclass
class Rectangle:
origin: Point
width: float
height: float
# Tipo alias per le forme geometriche
type Shape = Point | Circle | Rectangle
def describe_shape(shape: Shape) -> str:
# Analizza la forma e restituisce una descrizione testuale
match shape:
case Point(x=0, y=0):
return "origine del piano cartesiano"
case Point(x=x_val, y=y_val):
return f"punto alle coordinate ({x_val}, {y_val})"
case Circle(center=Point(x=0, y=0), radius=r):
return f"cerchio centrato nell'origine con raggio {r}"
case Circle(center=c, radius=r) if r > 100:
return f"cerchio grande in ({c.x}, {c.y}) con raggio {r}"
case Circle(center=c, radius=r):
return f"cerchio in ({c.x}, {c.y}) con raggio {r}"
case Rectangle(origin=o, width=w, height=h) if w == h:
return f"quadrato in ({o.x}, {o.y}) con lato {w}"
case Rectangle(origin=o, width=w, height=h):
return f"rettangolo in ({o.x}, {o.y}), {w}x{h}"
case _:
return "forma sconosciuta"
Il pattern matching si rivela particolarmente utile quando si lavora con strutture dati JSON o dizionari annidati, scenari molto comuni nelle applicazioni web e nelle API.
def handle_api_response(response: dict) -> str:
# Gestisce le diverse risposte dell'API tramite pattern matching
match response:
case {"status": "success", "data": {"users": [first, *rest]}}:
return f"trovati {1 + len(rest)} utenti, primo: {first}"
case {"status": "success", "data": {"users": []}}:
return "nessun utente trovato"
case {"status": "error", "code": 404, "message": msg}:
return f"risorsa non trovata: {msg}"
case {"status": "error", "code": code, "message": msg} if code >= 500:
return f"errore del server ({code}): {msg}"
case {"status": "error", **details}:
return f"errore generico: {details}"
case _:
return "risposta non riconosciuta"
Dataclass e modelli di dati
Le dataclass, introdotte in Python 3.7 e migliorate nelle versioni successive, offrono un modo dichiarativo per definire classi che fungono da contenitori di dati. Con l'aggiunta dei parametri slots, kw_only e match_args in Python 3.10, le dataclass sono diventate uno strumento ancora più potente e flessibile.
from dataclasses import dataclass, field
from datetime import datetime
@dataclass(frozen=True, slots=True)
class Product:
"""Modello immutabile di un prodotto con slot per efficienza di memoria."""
name: str
price: float
sku: str
# Campi con valori predefiniti
category: str = "generale"
tags: tuple[str, ...] = ()
def __post_init__(self) -> None:
# Validazione dei campi dopo l'inizializzazione
if self.price < 0:
raise ValueError("il prezzo non può essere negativo")
@dataclass(slots=True, kw_only=True)
class Order:
"""Ordine con campi obbligatoriamente nominati."""
customer_id: str
items: list[Product]
# Campi calcolati tramite factory
created_at: datetime = field(default_factory=datetime.now)
order_id: str = field(default_factory=lambda: __import__("uuid").uuid4().hex[:8])
@property
def total(self) -> float:
# Calcola il totale dell'ordine
return sum(item.price for item in self.items)
def add_item(self, product: Product) -> None:
# Aggiunge un prodotto all'ordine
self.items.append(product)
# Creazione con keyword arguments obbligatori
order = Order(
customer_id="C001",
items=[
Product(name="Tastiera meccanica", price=89.99, sku="KB-001"),
Product(name="Mouse ergonomico", price=49.99, sku="MS-002"),
]
)
Programmazione asincrona
Il modello asincrono di Python, basato su asyncio, ha ricevuto miglioramenti continui. Le versioni recenti hanno introdotto i task group, una gestione più robusta delle eccezioni con ExceptionGroup e un'API semplificata per i casi d'uso più comuni.
import asyncio
from collections.abc import AsyncIterator
async def fetch_data(url: str, delay: float) -> dict[str, str]:
# Simula una richiesta HTTP asincrona
await asyncio.sleep(delay)
return {"url": url, "status": "completato"}
async def process_urls(urls: list[str]) -> list[dict[str, str]]:
# Elabora più URL in parallelo usando un TaskGroup (Python 3.11+)
results: list[dict[str, str]] = []
async with asyncio.TaskGroup() as group:
tasks = [
group.create_task(fetch_data(url, delay=0.5))
for url in urls
]
# Tutti i task sono completati a questo punto
results = [task.result() for task in tasks]
return results
async def count_up_to(limit: int) -> AsyncIterator[int]:
# Generatore asincrono che produce numeri con un ritardo
for i in range(1, limit + 1):
await asyncio.sleep(0.1)
yield i
async def main() -> None:
urls = [
"https://api.example.com/users",
"https://api.example.com/products",
"https://api.example.com/orders",
]
# Esecuzione parallela con gestione strutturata degli errori
try:
results = await process_urls(urls)
for result in results:
print(result)
except* ValueError as error_group:
# Gestione delle eccezioni raggruppate (Python 3.11+)
for error in error_group.exceptions:
print(f"errore di valore: {error}")
except* ConnectionError as error_group:
for error in error_group.exceptions:
print(f"errore di connessione: {error}")
# Iterazione asincrona
async for number in count_up_to(5):
print(f"conteggio: {number}")
asyncio.run(main())
I gestori di contesto asincroni consentono di gestire risorse come connessioni a database, sessioni HTTP e pool di connessioni in modo sicuro e idiomatico all'interno di codice asincrono.
import asyncio
from contextlib import asynccontextmanager
from collections.abc import AsyncIterator
class ConnectionPool:
"""Pool di connessioni asincrono con gestione automatica delle risorse."""
def __init__(self, max_connections: int = 10) -> None:
# Inizializza il pool con un semaforo per il controllo della concorrenza
self._semaphore = asyncio.Semaphore(max_connections)
self._active_connections: int = 0
@asynccontextmanager
async def acquire(self) -> AsyncIterator["Connection"]:
# Acquisisce una connessione dal pool
await self._semaphore.acquire()
self._active_connections += 1
connection = Connection(pool=self)
try:
await connection.connect()
yield connection
finally:
# Rilascia la connessione al pool
await connection.disconnect()
self._active_connections -= 1
self._semaphore.release()
class Connection:
"""Rappresenta una singola connessione nel pool."""
def __init__(self, pool: ConnectionPool) -> None:
self._pool = pool
self._connected = False
async def connect(self) -> None:
# Simula l'apertura della connessione
await asyncio.sleep(0.01)
self._connected = True
async def disconnect(self) -> None:
# Simula la chiusura della connessione
self._connected = False
async def execute(self, query: str) -> list[dict]:
# Esegue una query sulla connessione
if not self._connected:
raise RuntimeError("connessione non attiva")
await asyncio.sleep(0.05)
return [{"query": query, "result": "dati di esempio"}]
Walrus operator e espressioni di assegnazione
L'operatore walrus (:=), introdotto in Python 3.8, consente di assegnare un valore a una variabile all'interno di un'espressione. Questo costrutto riduce la duplicazione di codice e migliora la leggibilità in diversi scenari come le list comprehension con filtri, i cicli di lettura e le condizioni composte.
import re
def extract_valid_emails(text: str) -> list[str]:
# Estrae gli indirizzi email validi usando il walrus operator
pattern = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
lines = text.strip().splitlines()
# Il walrus operator evita di eseguire la regex due volte
return [
match.group()
for line in lines
if (match := re.search(pattern, line)) is not None
]
def read_chunks(filepath: str, chunk_size: int = 1024) -> list[bytes]:
# Legge un file in blocchi usando il walrus operator nel ciclo
chunks: list[bytes] = []
with open(filepath, "rb") as file_handle:
while (chunk := file_handle.read(chunk_size)):
chunks.append(chunk)
return chunks
def find_first_long_word(words: list[str], min_length: int = 8) -> str | None:
# Trova la prima parola che supera la lunghezza minima
# e la restituisce in maiuscolo
for word in words:
if (cleaned := word.strip().lower()) and len(cleaned) >= min_length:
return cleaned.upper()
return None
Decoratori e metaprogrammazione
Python offre un sistema di decoratori flessibile che, combinato con i type hints moderni e le funzionalità del modulo functools, permette di implementare pattern avanzati di metaprogrammazione in modo leggibile e type-safe.
import functools
import time
import logging
from collections.abc import Callable
from typing import ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
logger = logging.getLogger(__name__)
def retry(
max_attempts: int = 3,
delay: float = 1.0,
exceptions: tuple[type[Exception], ...] = (Exception,),
) -> Callable[[Callable[P, R]], Callable[P, R]]:
"""Decoratore che ritenta l'esecuzione in caso di errore."""
def decorator(func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
last_exception: Exception | None = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as error:
last_exception = error
# Registra il tentativo fallito
logger.warning(
"tentativo %d/%d fallito per %s: %s",
attempt,
max_attempts,
func.__name__,
error,
)
if attempt < max_attempts:
time.sleep(delay * attempt)
# Rilancia l'ultima eccezione se tutti i tentativi falliscono
raise last_exception # type: ignore[misc]
return wrapper
return decorator
def cache_with_ttl(
ttl_seconds: float = 300.0,
) -> Callable[[Callable[P, R]], Callable[P, R]]:
"""Decoratore che implementa una cache con scadenza temporale."""
def decorator(func: Callable[P, R]) -> Callable[P, R]:
cache: dict[str, tuple[float, R]] = {}
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
# Genera una chiave unica basata sugli argomenti
cache_key = str(args) + str(sorted(kwargs.items()))
now = time.monotonic()
if cache_key in cache:
cached_time, cached_value = cache[cache_key]
if now - cached_time < ttl_seconds:
# Restituisce il valore dalla cache se non è scaduto
return cached_value
# Esegue la funzione e salva il risultato in cache
result = func(*args, **kwargs)
cache[cache_key] = (now, result)
return result
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5, exceptions=(ConnectionError, TimeoutError))
@cache_with_ttl(ttl_seconds=60.0)
def fetch_remote_config(endpoint: str) -> dict:
# Recupera la configurazione da un endpoint remoto
logger.info("richiesta configurazione da %s", endpoint)
return {"endpoint": endpoint, "data": "configurazione di esempio"}
Protocolli e tipizzazione strutturale
I protocolli, definiti nel modulo typing, permettono di implementare la tipizzazione strutturale in Python, un paradigma noto anche come duck typing statico. A differenza delle classi astratte, i protocolli non richiedono ereditarietà esplicita: una classe è conforme a un protocollo se implementa i metodi e gli attributi richiesti.
from typing import Protocol, runtime_checkable
from datetime import datetime
@runtime_checkable
class Serializable(Protocol):
"""Protocollo per oggetti serializzabili in dizionario."""
def to_dict(self) -> dict[str, object]: ...
class Persistable(Protocol):
"""Protocollo per oggetti che possono essere salvati e caricati."""
def save(self, path: str) -> None: ...
@classmethod
def load(cls, path: str) -> "Persistable": ...
class LogEntry:
"""Voce di log che implementa implicitamente Serializable."""
def __init__(self, message: str, level: str = "INFO") -> None:
self.message = message
self.level = level
self.timestamp = datetime.now()
def to_dict(self) -> dict[str, object]:
# Converte la voce di log in un dizionario
return {
"message": self.message,
"level": self.level,
"timestamp": self.timestamp.isoformat(),
}
class MetricRecord:
"""Record di metrica che implementa implicitamente Serializable."""
def __init__(self, name: str, value: float, unit: str) -> None:
self.name = name
self.value = value
self.unit = unit
def to_dict(self) -> dict[str, object]:
# Converte il record in un dizionario
return {
"name": self.name,
"value": self.value,
"unit": self.unit,
}
def export_records(records: list[Serializable]) -> list[dict[str, object]]:
# Esporta una lista di oggetti serializzabili
return [record.to_dict() for record in records]
# Entrambe le classi soddisfano il protocollo senza ereditarietà
entries: list[Serializable] = [
LogEntry("avvio del sistema", "INFO"),
MetricRecord("cpu_usage", 45.2, "percent"),
]
exported = export_records(entries)
Gestori di contesto e risorse
Oltre ai classici gestori di contesto basati su __enter__ e __exit__, Python moderno offre strumenti avanzati nel modulo contextlib che semplificano la gestione delle risorse, inclusi gestori multipli tramite ExitStack e la composizione di gestori con il decoratore contextmanager.
from contextlib import contextmanager, ExitStack
from collections.abc import Iterator
import tempfile
import os
@contextmanager
def temporary_directory(prefix: str = "app_") -> Iterator[str]:
"""Gestore di contesto per una directory temporanea con pulizia automatica."""
# Crea la directory temporanea
dir_path = tempfile.mkdtemp(prefix=prefix)
try:
yield dir_path
finally:
# Rimuove la directory e tutto il suo contenuto
import shutil
shutil.rmtree(dir_path, ignore_errors=True)
@contextmanager
def managed_file(path: str, mode: str = "w") -> Iterator:
"""Gestore di contesto con logging delle operazioni su file."""
print(f"apertura file: {path}")
file_handle = open(path, mode)
try:
yield file_handle
except Exception as error:
# Registra l'errore prima di propagarlo
print(f"errore durante l'operazione su {path}: {error}")
raise
finally:
file_handle.close()
print(f"file chiuso: {path}")
def process_multiple_files(file_paths: list[str]) -> dict[str, int]:
"""Elabora più file contemporaneamente con ExitStack."""
results: dict[str, int] = {}
# ExitStack gestisce automaticamente la chiusura di tutte le risorse
with ExitStack() as stack:
file_handles = [
stack.enter_context(open(path, "r"))
for path in file_paths
]
for path, handle in zip(file_paths, file_handles):
# Conta le righe non vuote in ciascun file
content = handle.read()
non_empty_lines = sum(
1 for line in content.splitlines() if line.strip()
)
results[os.path.basename(path)] = non_empty_lines
return results
F-string avanzate e formattazione
Le f-string, già presenti da Python 3.6, hanno ricevuto miglioramenti importanti. In Python 3.12 è stata rimossa la limitazione che impediva l'uso di backslash e commenti all'interno delle espressioni, e il supporto per le espressioni di debug con = (introdotto in Python 3.8) permette un'ispezione rapida delle variabili.
from datetime import datetime, timedelta
from decimal import Decimal
def demonstrate_fstring_features() -> None:
# Espressioni di debug con il suffisso =
username = "mario_rossi"
login_count = 42
print(f"{username=}, {login_count=}")
# Stampa: username='mario_rossi', login_count=42
# Formattazione numerica avanzata
revenue = Decimal("1234567.89")
percentage = 0.8534
print(f"fatturato: {revenue:,.2f} EUR")
print(f"percentuale: {percentage:.1%}")
# Allineamento e padding
items = [("CPU", 299.99), ("RAM", 89.50), ("SSD", 149.00)]
for name, price in items:
# Allinea il nome a sinistra e il prezzo a destra
print(f"{name:<10} {price:>10.2f} EUR")
# Formattazione delle date
now = datetime.now()
print(f"data corrente: {now:%d/%m/%Y %H:%M}")
deadline = now + timedelta(days=30)
print(f"scadenza: {deadline:%A %d %B %Y}")
# Espressioni complesse nelle f-string (Python 3.12+)
data = {"scores": [85, 92, 78, 96, 88]}
print(f"media: {sum(data['scores']) / len(data['scores']):.1f}")
# Conversioni con !r, !s, !a
raw_input = "testo con\ttabulazione"
print(f"rappresentazione: {raw_input!r}")
print(f"ascii safe: {raw_input!a}")
Enumerazioni moderne
Il modulo enum ha ricevuto aggiornamenti significativi, tra cui StrEnum e IntEnum, che facilitano l'interoperabilità con stringhe e interi, e il decoratore @verify per garantire la correttezza delle definizioni.
from enum import Enum, StrEnum, auto, verify, UNIQUE
@verify(UNIQUE)
class HttpStatus(Enum):
"""Codici di stato HTTP con metodi di utilità."""
OK = 200
CREATED = 201
BAD_REQUEST = 400
UNAUTHORIZED = 401
FORBIDDEN = 403
NOT_FOUND = 404
INTERNAL_ERROR = 500
@property
def is_success(self) -> bool:
# Verifica se lo stato indica successo
return 200 <= self.value < 300
@property
def is_client_error(self) -> bool:
# Verifica se lo stato indica un errore del client
return 400 <= self.value < 500
class Permission(StrEnum):
"""Permessi del sistema come stringhe per serializzazione diretta."""
READ = auto()
WRITE = auto()
DELETE = auto()
ADMIN = auto()
class Role:
"""Ruolo con un insieme di permessi associati."""
# Definizione dei ruoli con i rispettivi permessi
_role_permissions: dict[str, set[Permission]] = {
"viewer": {Permission.READ},
"editor": {Permission.READ, Permission.WRITE},
"admin": {Permission.READ, Permission.WRITE, Permission.DELETE, Permission.ADMIN},
}
def __init__(self, name: str) -> None:
self.name = name
# Recupera i permessi associati al ruolo
self.permissions = self._role_permissions.get(name, set())
def has_permission(self, permission: Permission) -> bool:
# Controlla se il ruolo possiede il permesso specificato
return permission in self.permissions
Itertools e programmazione funzionale
La libreria standard di Python offre strumenti potenti per la programmazione funzionale attraverso i moduli itertools e functools. Combinati con le comprehension e le espressioni generatrici, questi strumenti consentono di scrivere pipeline di trasformazione dati concise ed efficienti in termini di memoria.
import itertools
import functools
from collections.abc import Iterator
def chunked[T](iterable: Iterator[T], size: int) -> Iterator[tuple[T, ...]]:
"""Divide un iterabile in blocchi di dimensione fissa."""
# Crea un iteratore e lo raggruppa in tuple di lunghezza size
iterator = iter(iterable)
while batch := tuple(itertools.islice(iterator, size)):
yield batch
def pipeline_example(raw_data: list[dict[str, object]]) -> list[str]:
"""Esempio di pipeline funzionale per l'elaborazione dei dati."""
# Filtra i record attivi
active_records = filter(
lambda record: record.get("active", False),
raw_data,
)
# Estrae e trasforma i nomi
names: Iterator[str] = map(
lambda record: str(record.get("name", "")).upper(),
active_records,
)
# Rimuove i duplicati mantenendo l'ordine
seen: set[str] = set()
def unique(item: str) -> bool:
# Restituisce True solo alla prima occorrenza
if item in seen:
return False
seen.add(item)
return True
return list(filter(unique, names))
def compose(*functions):
"""Compone più funzioni in una singola funzione."""
# Applica le funzioni da destra a sinistra
def composed(value):
return functools.reduce(
lambda result, func: func(result),
reversed(functions),
value,
)
return composed
# Esempio di composizione
normalize = compose(
str.strip,
str.lower,
lambda s: s.replace(" ", " "),
)
Exception group e gestione avanzata degli errori
Python 3.11 ha introdotto le ExceptionGroup e la sintassi except*, che permettono di gestire più eccezioni simultanee in modo strutturato. Questa funzionalità è particolarmente utile nei contesti di programmazione concorrente, dove più operazioni possono fallire contemporaneamente.
class ValidationError(Exception):
"""Errore di validazione con campo e messaggio."""
def __init__(self, field: str, message: str) -> None:
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
class NetworkError(Exception):
"""Errore di rete con codice di stato."""
def __init__(self, status_code: int, detail: str) -> None:
self.status_code = status_code
self.detail = detail
super().__init__(f"HTTP {status_code}: {detail}")
def validate_form(data: dict[str, str]) -> None:
"""Valida un modulo raccogliendo tutti gli errori."""
errors: list[ValidationError] = []
if not data.get("email"):
errors.append(ValidationError("email", "campo obbligatorio"))
elif "@" not in data["email"]:
errors.append(ValidationError("email", "formato non valido"))
if not data.get("password"):
errors.append(ValidationError("password", "campo obbligatorio"))
elif len(data["password"]) < 8:
errors.append(ValidationError("password", "almeno 8 caratteri"))
if not data.get("username"):
errors.append(ValidationError("username", "campo obbligatorio"))
# Lancia tutte le eccezioni come gruppo
if errors:
raise ExceptionGroup("errori di validazione", errors)
def handle_form_submission(data: dict[str, str]) -> str:
"""Gestisce l'invio del modulo con gestione strutturata degli errori."""
try:
validate_form(data)
return "modulo valido"
except* ValidationError as error_group:
# Gestisce solo gli errori di validazione dal gruppo
messages = [
f"- {err.field}: {err.message}"
for err in error_group.exceptions
]
return "errori trovati:\n" + "\n".join(messages)
Conclusione
Le caratteristiche moderne di Python hanno trasformato profondamente il modo in cui scriviamo codice. La tipizzazione avanzata consente di individuare errori prima dell'esecuzione, il pattern matching semplifica la logica condizionale complessa, le dataclass riducono il boilerplate nella modellazione dei dati, e la programmazione asincrona strutturata rende più sicura la gestione della concorrenza. L'adozione progressiva di queste funzionalità porta a un codice più robusto, espressivo e manutenibile, senza sacrificare la semplicità e la leggibilità che hanno reso Python uno dei linguaggi più apprezzati nel panorama dello sviluppo software contemporaneo.