Le funzionalità di Python che ho imparato troppo tardi

Match statement, il walrus operator, il debug con f-string, le dataclass e altre funzionalità di Python che mi avrebbero risparmiato ore se le avessi conosciute prima.

Le funzionalità di Python che ho imparato troppo tardi

Ogni linguaggio ha quelle funzionalità che scopri un anno dopo che ti servivano, e la reazione è sempre la stessa: “aspetta, c’era da tutto questo tempo?” Ecco la mia lista per Python.

Dataclass (3.7+)

Prima di conoscere le dataclass, scrivevo __init__, __repr__ e __eq__ a mano per ogni contenitore di dati. Una perdita di tempo totale.

from dataclasses import dataclass

@dataclass
class Trade:
    symbol: str
    price: float
    quantity: int
    side: str = "buy"  # valore predefinito

Tutto qui. Ottieni __init__, __repr__, __eq__ e ordinamento opzionale gratis. Aggiungi frozen=True se vuoi l’immutabilità. La classe qui sopra sostituisce circa 20 righe di boilerplate.

Se stai ancora scrivendo classi con nient’altro che self.x = x dentro __init__, fermati e usa una dataclass. Il te stesso del futuro ti ringrazierà.

Il walrus operator := (3.8+)

Assegna un valore e usalo nella stessa espressione. Sembra banale, ma elimina un tipo specifico di duplicazione fastidiosa:

# Prima: calcola, poi controlla
line = f.readline()
while line:
    process(line)
    line = f.readline()

# Dopo: assegna e controlla in un solo passaggio
while (line := f.readline()):
    process(line)

Ottimo anche nelle list comprehension con chiamate costose:

results = [
    clean
    for raw in data
    if (clean := expensive_transform(raw)) is not None
]

Non abusarne. Se l’espressione è già semplice, := aggiunge solo rumore. Ma quando evita di chiamare la stessa funzione due volte, è perfetto.

Debug con f-string (3.8+)

Aggiungi = dopo un’espressione in una f-string e Python stampa sia l’espressione che il suo valore:

x = 42
print(f"{x = }")       # stampa: x = 42
print(f"{x * 2 = }")   # stampa: x * 2 = 84
print(f"{len(data) = }")  # stampa: len(data) = 1500

Prima scrivevo print(f"x: {x}") ovunque. Il trucco con = è più veloce, meno soggetto a errori e si auto-documenta. Non tornerai più indietro.

I match statement (3.10+)

La versione Python del pattern matching. Se hai usato match in Rust o Scala, ti sembrerà familiare (anche se meno potente). Se finora hai incatenato blocchi if/elif, ecco l’upgrade:

match command.split():
    case ["quit"]:
        exit_game()
    case ["move", direction]:
        player.move(direction)
    case ["pick", "up", item]:
        player.pick_up(item)
    case _:
        print("Unknown command")

La vera potenza è il matching strutturale — puoi destrutturare dizionari, oggetti e strutture annidate:

match event:
    case {"type": "click", "x": x, "y": y}:
        handle_click(x, y)
    case {"type": "scroll", "delta": d} if d > 0:
        scroll_up(d)
    case {"type": "scroll", "delta": d}:
        scroll_down(abs(d))

È strettamente necessario? No. if/elif funziona ancora. Ma match rende leggibile la logica di dispatch complessa, e la leggibilità è tutto.

Type hint che aiutano davvero (3.9+)

Non hai più bisogno di from typing import List, Dict, Optional. I generici integrati funzionano direttamente:

# Vecchio modo
from typing import List, Dict, Optional

def process(items: List[Dict[str, Optional[int]]]) -> None: ...

# Modo moderno (3.9+)
def process(items: list[dict[str, int | None]]) -> None: ...

La sintassi X | Y per i tipi union (3.10+) elimina Optional e Union in un colpo solo. Combinala con mypy o pyright e catturi i bug prima che arrivino in produzione. Non a runtime — Python non verifica i tipi a runtime — ma nel tuo editor e nella pipeline CI.

Merge di dict | dict (3.9+)

defaults = {"color": "blue", "size": "medium"}
overrides = {"size": "large", "shape": "circle"}

merged = defaults | overrides
# {'color': 'blue', 'size': 'large', 'shape': 'circle'}

Niente più {**a, **b}. L’operatore | è più pulito e puoi usare |= per il merge in-place. Una piccola cosa, ma salta fuori continuamente nella gestione delle configurazioni.

itertools.batched() (3.12+)

Quante volte hai scritto un ciclo per dividere una lista in gruppi di N? Troppe. Adesso è integrato:

from itertools import batched

list(batched("ABCDEFG", 3))
# [('A', 'B', 'C'), ('D', 'E', 'F'), ('G',)]

Se sei bloccato su un Python più vecchio, more-itertools ha chunked() che fa la stessa cosa.

Il pattern

La maggior parte di queste funzionalità condivide un tratto: riducono il boilerplate. Python è sempre stato bravo a dire quello che intendi in meno righe, e ogni release porta questo concetto un passo avanti. Il costo di non aggiornarsi non è che il tuo codice si rompe — è che scrivi più codice del necessario, e più codice significa più bug.

Scegli una funzionalità da questa lista che non hai ancora usato, provala nella tua prossima PR, e guarda quanto velocemente diventa un’abitudine.