Linee guida Python che uso davvero

PEP 8 nella pratica, gli idiomi che fanno sembrare il codice Python davvero Python e le novità del linguaggio degli ultimi anni che vale la pena conoscere.

Ogni team in cui sono entrato aveva il suo stile Python non scritto. La buona notizia è che c’è molta sovrapposizione, e la maggior parte dei disaccordi riguarda cose che non contano. Ecco la versione che ho messo insieme dopo anni di commenti nei pull request — le regole che mi tengo davvero, quelle contro cui combatto, e le novità del linguaggio che secondo me vale la pena usare.

Comincia dalla parte noiosa: i tool

Prima di discutere di stile, automatizza le parti su cui non hai bisogno di discutere:

pip install ruff black mypy
  • ruff — linter veloce e sorter di import, sostituisce circa sei strumenti più vecchi (flake8, isort, pyupgrade, ecc.). Mettilo come pre-commit hook. Configuralo per imporre PEP 8 più le regole su cui il tuo team è d’accordo, poi non discutere mai più di quelle regole.
  • black — formatter, opinionato e inflessibile di proposito. Il punto non è che le scelte di black siano perfette, è che nessuno nel tuo team deve farle. (Anche ruff ora ha un formatter, ed è drop-in compatibile con black se preferisci usare un solo strumento per tutto.)
  • mypy — type checker statico. Più sotto sui type hint.

Il novanta percento dei dibattiti sullo stile evapora il giorno in cui adotti i formatter. Spendi quell’energia da qualche parte dove conta davvero.

Naming e forma

PEP 8 è quasi tutto buon senso:

# Variabili e funzioni: snake_case
order_count = 0

def parse_invoice(raw: str) -> Invoice:
    ...

# Classi: PascalCase
class InvoiceParser:
    ...

# Costanti: UPPER_SNAKE_CASE
MAX_RETRY_ATTEMPTS = 5

# I nomi "interni" iniziano con un underscore
def _build_url(host: str, path: str) -> str:
    ...

Le due regole che aggiungerei:

  • Niente variabili di una sola lettera fuori da i in un loop stretto o x, y in una coordinata. c per “customer” sembra innocente finché sei mesi dopo non stai fissando c.c chiedendoti cosa fosse il secondo c.
  • I nomi delle funzioni dovrebbero essere verbi, i nomi delle variabili sostantivi. parse_invoice non invoice_parser. customer non get_customer.

Cose che sembrano Python

Il segno più chiaro che qualcuno sta scrivendo Python come Python vuole essere scritto:

# f-string per tutto (dalla 3.6). Salta .format(), salta %.
print(f"Caricate {len(rows):,} righe in {duration:.2f}s")

# Comprehension al posto di catene di map/filter, quando ci stanno su una riga.
active_ids = [u.id for u in users if u.is_active]

# Una generator expression quando non hai bisogno di tutta la lista in una volta.
total = sum(order.amount for order in orders)

# Comprehension di dict/set, stessa idea.
by_id = {u.id: u for u in users}

# Unpacking ovunque rende l'intento più chiaro.
first, *middle, last = path.parts
name, _, ext = filename.rpartition(".")

# Usa enumerate, non range(len(...)).
for i, row in enumerate(rows, start=1):
    print(f"{i}: {row}")

# Usa zip quando iteri due sequenze in lockstep.
for old, new in zip(old_rows, new_rows, strict=True):
    diff(old, new)

Quello strict=True su zip (aggiunto in 3.10) è una delle piccole cose che si ripagano subito: solleva ValueError se le sequenze hanno lunghezze diverse, invece di scartare silenziosamente la coda. Ora uso strict=True di default e lo tolgo solo quando ho un motivo vero.

I type hint, e quanto prenderli sul serio

Aggiungi type hint a ogni firma di funzione nel codice che pensi di tenere. Dentro il corpo delle funzioni, annota solo quando il tipo non è ovvio dal contesto. Questo cattura un numero sorprendente di bug a livello di IDE — mypy è un extra carino, ma è la vittoria sull’autocomplete che rende i type hint degni dei tasti che premi.

from collections.abc import Iterable
from pathlib import Path

def load_invoices(paths: Iterable[Path]) -> list[Invoice]:
    return [parse_invoice(p.read_text()) for p in paths]

Due note sulla sintassi moderna:

  • list[Invoice] invece di List[Invoice] — i generici built-in funzionano dalla 3.9. Per i container di base non devi più importare da typing.
  • X | None invece di Optional[X] — la sintassi union (PEP 604) funziona dalla 3.10. Meno rumore di import, più facile da leggere.
def find_customer(customer_id: int) -> Customer | None:
    ...

Non annoto le variabili dentro il corpo delle funzioni a meno che il tipo non sia genuinamente ambiguo. count = 0 non ha bisogno di count: int = 0. L’annotazione è rumore; l’inferenza basta.

Usa pathlib, non os.path

Se stai ancora facendo os.path.join(base, "data", "file.csv"), cambia.

from pathlib import Path

base = Path("/var/data")
target = base / "exports" / "2026-04" / "invoices.csv"

target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(rendered_csv, encoding="utf-8")

for csv_file in base.rglob("*.csv"):
    process(csv_file)

Path sa fare join, glob, lettura, scrittura, controllo di esistenza e risoluzione di path relativi, tutto su un solo oggetto. Il giorno in cui ho cambiato la mia memoria muscolare da os.path a pathlib è il giorno in cui il mio codice di gestione file ha smesso di sembrare un museo.

Il tricheco, il match e le altre novità

Alcune feature uscite negli ultimi rilasci che trovo davvero utili:

Le assignment expression (:=, l‘“operatore tricheco”), 3.8. Usalo con parsimonia — è potente e brutto. L’unico posto dove vale chiaramente la pena è quando altrimenti calcoleresti la stessa espressione due volte:

# Senza
data = fetch()
if data:
    process(data)

# Con — va bene, leggermente più compatto
if data := fetch():
    process(data)

# Dove brilla davvero: evitare doppio lavoro in una comprehension
results = [
    parsed
    for line in lines
    if (parsed := parse(line)) is not None
]

Pattern matching strutturale (match/case), 3.10. Non usarlo come un if/elif elegante. Usalo quando stai davvero destrutturando una forma:

def describe(message: dict) -> str:
    match message:
        case {"type": "order", "amount": amount} if amount > 1000:
            return f"ordine grosso: {amount}"
        case {"type": "order", "amount": amount}:
            return f"ordine: {amount}"
        case {"type": "refund", "reason": reason}:
            return f"rimborso: {reason}"
        case _:
            return "messaggio sconosciuto"

match si guadagna il pane. Per una catena di if x == 1: ... elif x == 2:, il vecchio if/elif è ancora più chiaro.

Exception group e except*, 3.11. Quando esegui più cose in concorrenza, a volte vuoi catturare diversi errori contemporaneamente invece del solo primo. ExceptionGroup e except* ti danno questo. Non li uso spesso, ma quando mi servono sono lo strumento giusto.

tomllib nella standard library, 3.11. Finalmente puoi leggere file di configurazione TOML senza una dipendenza di terze parti:

import tomllib
from pathlib import Path

with Path("pyproject.toml").open("rb") as f:
    config = tomllib.load(f)

(Non c’è tomllib.dump() — la standard library legge solo TOML, non lo scrive. Per scriverlo serve ancora tomli-w o tomlkit. Inciampa la gente.)

La nota sugli “ultimi aggiornamenti”

Il rilascio a cui presterei attenzione adesso è Python 3.13 (ottobre 2024), che ha finalmente portato a casa due esperimenti che andavano avanti da anni:

  • Una build sperimentale free-threaded (no-GIL) — il global interpreter lock può essere disabilitato a tempo di build. È un interprete separato python3.13t, non quello di default, e la maggior parte delle estensioni C deve fare opt-in. Non ci metterei codice di produzione per ora, ma è il rilascio Python più interessante in un decennio per chiunque abbia mai perso un weekend dietro a workaround del GIL.
  • Un nuovo REPL interattivo con editing multilinea, syntax highlighting e history come si deve. Preso in prestito da PyPy. È piccolo ma è il tipo di piccolo che ti rende più felice ogni singolo giorno.

C’è anche un goccia continua di miglioramenti a typing (PEP 695 per la sintassi dei tipi generici in 3.12, ReadOnly per TypedDict in 3.13, TypeIs per narrowing più puliti) — vale la pena scorrere la pagina “What’s New” a ogni rilascio, anche se non adotti subito i giocattoli nuovi.

Il riassunto più corto possibile

Se dovessi scrivere le regole su una cartolina:

  1. Lascia che il formatter prenda le decisioni di formattazione.
  2. Aggiungi type hint alle firme delle funzioni.
  3. Usa pathlib, f-string, comprehension ed enumerate.
  4. zip(..., strict=True) di default.
  5. Non andare a cercare la sintassi cervellotica finché quella semplice non smette davvero di funzionare.

È tutto. Il resto è gusto, e il gusto si sviluppa leggendo il codice degli altri, non memorizzando i PEP.