Benvenuto alla lezione tre. Oggi vediamo tre feature sintattiche che, messe insieme, hanno cambiato il modo in cui chi è fluente in Python scrive codice di tutti i giorni: le f-string (3.6), il walrus operator (3.8) e il pattern matching (3.10). Nessuna di esse, da sola, è sconvolgente. Tutte e tre insieme, usate nei posti giusti, sono la differenza tra codice che si legge come 2026 e codice che si legge come l’interno di un manuale del 2014.
Un tema attraversa tutte e tre: ognuna sostituisce un idioma vecchio più goffo, ma ognuna ha anche casi in cui l’idioma vecchio è ancora migliore. La skill è sapere quale è quale. Vediamo ogni feature, nel modo in cui la userai davvero, e i casi in cui ricorrervi peggiora le cose invece di migliorarle.
f-string: la sola formattazione di stringhe a cui ricorrere per prima
Prima delle f-string (Python 3.6, dicembre 2016), avevi tre modi per formattare le stringhe:
name = "Alice"
age = 30
# 1. L'operatore "%" stile C: più vecchio della polvere, funziona ancora
print("Hello, %s, you are %d" % (name, age))
# 2. Il metodo .format(): aggiunto nella 2.6, era il modo "moderno" fino alla 3.6
print("Hello, {}, you are {}".format(name, age))
# 3. Concatenazione di stringhe: il modo "ho appena imparato Python ieri"
print("Hello, " + name + ", you are " + str(age))
Tutti e tre funzionano ancora, tutti e tre compaiono ancora nelle codebase vecchie, e tutti e tre sono ormai sbagliati di default. La risposta moderna:
print(f"Hello, {name}, you are {age}")
Due caratteri di overhead (il prefisso f), zero overhead cognitivo, e le variabili si leggono nell’ordine in cui le diresti ad alta voce. Le f-string non sono solo più corte di .format(): sono notevolmente più veloci (il bytecode è più diretto), si integrano col debugger in modo più pulito, e gestiscono praticamente ogni caso che le forme più vecchie gestivano.
Il toolkit completo delle f-string
# Interpolazione di base
user = "Alice"
greeting = f"Hello, {user}" # "Hello, Alice"
# Espressioni, non solo variabili
items = [1, 2, 3, 4, 5]
print(f"You have {len(items)} items, summing to {sum(items)}")
# Chiamate di metodo e accesso ad attributi
import datetime
now = datetime.datetime.now()
print(f"Today is {now.strftime('%A')}")
# Specificatori di formato: stessa sintassi di .format(), dopo i due punti
price = 1234.5678
print(f"Price: {price:.2f}") # "Price: 1234.57"
print(f"Price: {price:,.2f}") # "Price: 1,234.57" (separatore migliaia)
print(f"Pad: {price:>12.2f}") # allineamento a destra in 12 colonne
print(f"Pct: {0.4567:.1%}") # "Pct: 45.7%"
print(f"Hex: {255:#x}") # "Hex: 0xff"
# Formattazione di date
print(f"Date: {now:%Y-%m-%d %H:%M}") # "Date: 2026-05-01 14:30"
# La sintassi di debug della 3.8: stampa nome E valore
x = 42
print(f"{x=}") # "x=42"
print(f"{len(items)=}") # "len(items)=5"
La sintassi {x=} (3.8+) è una di quelle cose silenziosamente trasformative. Sostituisci dieci anni di print("x =", x) con print(f"{x=}") e risparmi battiture per il resto della tua carriera. Usala costantemente per il debugging.
Quando NON usare le f-string
Due casi. Memorizzali.
Logging. Questo è sbagliato:
import logging
log = logging.getLogger(__name__)
# NON farlo
log.info(f"Processing user {user_id} with payload {expensive_to_format(payload)}")
La f-string viene valutata prima che log.info venga chiamato. Se il livello di log è impostato a WARNING e questo è un messaggio INFO, il messaggio viene scartato, ma expensive_to_format(payload) è già girato. Il modulo logging usa deliberatamente la formattazione lazy % esattamente per questo motivo:
log.info("Processing user %s with payload %s", user_id, expensive_to_format(payload))
Gli argomenti vengono formattati solo se il messaggio sarà davvero emesso. Su un servizio ad alto volume questo conta.
Query SQL (e qualunque stringa che entri nel parser di un altro linguaggio). Questa è una vulnerabilità:
# MAI fare così. SQL injection.
cursor.execute(f"SELECT * FROM users WHERE name = '{user_input}'")
Usa query parametrizzate. Sempre. Ogni driver di database le supporta. Le f-string sono lo strumento sbagliato per qualunque stringa che attraversi un confine tra linguaggi entrando in un parser: SQL, comandi di shell, HTML, qualunque cosa. Lo strumento giusto è l’API progettata per gestire l’escaping al posto tuo.
Per tutto il resto, il 95% delle stringhe che costruisci in un programma normale, le f-string sono corrette.
Il walrus operator: assegnazione come espressione
Python 3.8 ha aggiunto :=, ufficialmente chiamato “assignment expression” ma universalmente noto come walrus operator (perché := sembra un tricheco, se inclini la testa e hai il giusto tipo di immaginazione).
Fa una sola cosa: assegna un valore a un nome e restituisce il valore, in una singola espressione. Sembra noioso finché non vedi cosa ti permette di evitare.
Il caso classico: loop di lettura-e-test
# Pre-walrus
with open("data.log") as f:
line = f.readline()
while line:
process(line)
line = f.readline()
Quel f.readline() compare due volte. Facile dimenticarne uno, facile introdurre un bug se cambi il modo di leggere. Il walrus lo collassa:
# Con walrus
with open("data.log") as f:
while line := f.readline():
process(line)
Un solo riferimento a f.readline(), niente duplicazione, il loop si legge in modo pulito: “finché line è qualcosa, processala”.
Altri casi davvero buoni
Evitare una chiamata di funzione ridondante in una comprehension:
# Senza walrus: chiama expensive_compute(x) due volte per gli elementi che passano
results = [expensive_compute(x) for x in inputs if expensive_compute(x) > threshold]
# Con walrus: una volta sola
results = [y for x in inputs if (y := expensive_compute(x)) > threshold]
Match di regex compilate:
import re
pattern = re.compile(r"user_(\d+)")
# Senza walrus
match = pattern.match(text)
if match:
user_id = int(match.group(1))
process_user(user_id)
# Con walrus
if match := pattern.match(text):
process_user(int(match.group(1)))
Il corpo dell’if usa chiaramente match, e la variabile è bound solo quando c’è davvero un match.
Quando NON usare il walrus
Ovunque renda il codice più difficile da leggere. Il walrus è uno strumento di chiarezza, non uno strumento di brevità. Se usarlo costringe chi legge a tornare indietro per capire da dove arriva una variabile, l’hai peggiorato.
# Questo è peggio dell'alternativa.
# Il walrus è sepolto, l'assegnazione non è ovvia,
# e hai risparmiato una riga di codice al costo di un lettore confuso.
total = sum((y := x * 2) + 1 for x in range(10) if (y > 5))
Una buona regola: se non riesci a spiegare cosa fa il walrus in una breve frase, non usarlo. La maggior parte del codice non ne ha bisogno. Ricorrici quando c’è una vera vincita di leggibilità (il loop di lettura file, il match regex, il caso di evitare ricalcolo in una comprehension) e saltalo il resto del tempo.
Pattern matching: non è solo uno switch elegante
Python 3.10 (ottobre 2021) ha aggiunto match e case. Il framing nei primi articoli, “Python ha finalmente uno switch!”, lo sottovalutava. Lo switch di C/Java è una glorificata jump table su costanti intere. Il match di Python è structural pattern matching: fa match sulla forma dei dati, la destruttura in variabili, e si integra con classi e dataclass in un modo che cambia davvero come si scrivono certi tipi di codice.
La forma di base
def http_error_message(status: int) -> str:
match status:
case 200 | 201 | 204:
return "OK"
case 301 | 302:
return "Redirect"
case 400:
return "Bad request"
case 401 | 403:
return "Unauthorized"
case 404:
return "Not found"
case 500 | 502 | 503 | 504:
return "Server error"
case _:
return f"Unknown status {status}"
Quel case _: è il wildcard, equivalente a default: in C. Il | separa alternative. Fin qui è, in effetti, uno switch elegante.
Dove diventa interessante: shape matching
match brilla quando stai diramando sulla forma di un valore, non solo sulla sua identità.
def handle_message(msg: dict) -> str:
match msg:
case {"type": "ping"}:
return "pong"
case {"type": "greeting", "name": name}:
return f"Hello, {name}"
case {"type": "command", "action": action, "args": [first, *rest]}:
return f"Running {action} with {first} and {len(rest)} more args"
case {"type": "error", "code": code} if code >= 500:
return f"Server error: {code}"
case {"type": "error", "code": code}:
return f"Client error: {code}"
case _:
return "Unknown message"
Cose da notare:
{"type": "greeting", "name": name}fa match su un dict con una chiave"type"uguale a"greeting"e fa il binding del valore di"name"a una variabile locale chiamataname.[first, *rest]è destrutturazione di lista: il primo elemento afirst, il resto arest.if code >= 500è una guard: condizione extra che deve essere vera perché il case faccia match.- L’ordine dei case conta. Python li controlla dall’alto verso il basso, prende il primo match.
Questo è davvero potente per parsare messaggi JSON, attraversare nodi AST, fare dispatching su record tipo enum, o qualunque codice sia una catena di “se questo dict ha queste chiavi con queste forme, fai X”.
Pattern di classe
Il pattern matching si integra con le classi tramite l’attributo __match_args__ (impostato automaticamente sulle dataclass).
from dataclasses import dataclass
@dataclass
class Circle:
radius: float
@dataclass
class Rectangle:
width: float
height: float
@dataclass
class Triangle:
base: float
height: float
def area(shape: Circle | Rectangle | Triangle) -> float:
match shape:
case Circle(radius=r):
return 3.14159 * r * r
case Rectangle(width=w, height=h):
return w * h
case Triangle(base=b, height=h):
return 0.5 * b * h
Strutturalmente è simile ai tipi di dati algebrici di Rust o Haskell. Non è espressivo come quelli (il pattern matching di Python è intenzionalmente meno stringente) ma per i casi in cui calza, calza splendidamente. Anche i type checker usano il pattern di match-class per restringere i tipi automaticamente.
Capture vs comparison: la trappola
C’è una trappola che frega tutti esattamente una volta. Guarda:
HOST = "localhost"
def check(target: str) -> str:
match target:
case HOST:
return "matches host"
case _:
return "no match"
print(check("anything")) # "matches host" <- aspetta, cosa?
Il case HOST: non è “match se target è uguale a HOST”. Un nome nudo in un case è un capture pattern: fa il binding di qualunque cosa fosse in target a un nuovo nome locale HOST (oscurando quello a livello di modulo). Ogni valore fa match.
Per confrontare con una costante, devi qualificarla:
class Config:
HOST = "localhost"
def check(target: str) -> str:
match target:
case Config.HOST: # nome puntato = comparison
return "matches host"
case _:
return "no match"
Oppure usa direttamente un letterale. Oppure membri di enum.Enum. La regola: i nomi puntati confrontano, i nomi nudi minuscoli catturano. Una volta che ti ha morso questa, te lo ricordi per sempre.
Quando NON usare match
Due casi.
Un semplice if/elif è più corto e chiaro. Se stai facendo match su due o tre valori interi senza destrutturazione, una catena if/elif va bene ed è probabilmente più leggibile. match brilla con quattro o più rami con shape matching, non con “x è uguale a 1, 2 o 3”.
I dati non hanno una forma consistente. match è brillante quando i tuoi dati hanno pattern prevedibili. Se ogni case è case _ if some_complicated_condition:, hai reinventato if/elif con passaggi extra. La struttura ci deve essere perché match aggiunga valore.
Mettendo tutto insieme
Ecco una piccola funzione che usa tutte e tre le feature in un modo che, nel 2020, sarebbe stato tre volte più lungo:
import logging
import re
log = logging.getLogger(__name__)
USER_PATTERN = re.compile(r"user:(\d+)")
def parse_event(event: dict) -> str:
match event:
case {"type": "click", "target": target} if (m := USER_PATTERN.match(target)):
user_id = int(m.group(1))
log.info("Click on user %s", user_id)
return f"User clicked: {user_id=}"
case {"type": "view", "page": page}:
log.info("Page view: %s", page)
return f"Viewed {page}"
case _:
log.warning("Unknown event: %s", event)
return "Unknown event"
f-string per i valori di ritorno rivolti agli umani e per il debug {user_id=}. Walrus dentro la guard if per fare il binding del match regex. Pattern matching per fare dispatching sulla forma dell’evento. Formattazione lazy % nelle chiamate a log perché è quello che logging vuole.
Riepilogo
f-string: default per le stringhe rivolte agli umani, eccetto logging e SQL. Usa {x=} per il debugging, mi ringrazierai. Walrus: ricorrici sui loop di lettura-e-test e dove rimuove duplicazione; saltalo quando costa leggibilità. match: brilla sullo shape matching e sui dati strutturati; non è uno switch, non pensarlo come tale; ricorda la trappola della cattura su nome nudo.
La lezione 4 (venerdì) è su iteratori, generators e il protocollo di iterazione. La vera sala macchine di Python: una volta che la capisci, metà della standard library di colpo ha senso.
Letture di approfondimento
- PEP 498 - Literal String Interpolation: la proposta delle f-string.
- PEP 572 - Assignment Expressions: il walrus, inclusa la discussione che ha quasi spaccato la community nel 2018.
- PEP 634 - Structural Pattern Matching: Specification: la spec formale.
- PEP 636 - Structural Pattern Matching: Tutorial: la compagna leggibile.