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.