Benvenuto al Modulo 7. Abbiamo passato le ultime dodici lezioni a imparare ad analizzare i dati: caricare un file, affettarlo, raggrupparlo, plottarlo, scrivere la risposta. Quello è il lato consumatore del mondo dei dati. Il lato produttore (l’ingegneria che mette un dataset pulito sul disco dell’analista in primo luogo) è di cosa parlano le prossime lezioni. Ed è anche una frazione molto grande dei lavori reali e pagati che hanno “Python” nel titolo nel 2026.
La forma più comune che questo lavoro prende è ETL: extract, transform, load. Tre lettere che definiscono una pipeline. Leggi i dati da qualche parte, ne cambi la forma, e li scrivi da qualche altra parte. Praticamente ogni job batch sui dati nell’industria è una variante di questo. Oggi camminiamo attraverso il pattern, le varianti moderne (ELT, medallion, lakehouse: i buzzword che il tuo manager userà) e le decisioni di design che fai ogni volta che ti siedi a costruirne uno.
I tre stadi, disaccoppiati
La ragione per cui ETL è un modello mentale utile e non solo una descrizione è che i tre stadi hanno modalità di fallimento diverse e beneficiano dell’essere tenuti separati.
Extract è dove parli con il mondo esterno. Le API si rompono, i database rallentano, ci sono blip di rete, i file arrivano in ritardo o scritti a metà. Tutto ciò che fallisce perché la realtà è caotica fallisce qui.
Transform è pura computazione. Dato lo stesso input produce sempre lo stesso output. Nessuna dipendenza esterna, nessun orologio, nessuna rete. Se un transform fallisce, è un bug nel tuo codice, non una giornata storta sul server di qualcun altro.
Load è l’altro lato del confine caotico: scrivere su una destinazione che ha le sue regole su unicità, ordinamento, transazioni e rate limit.
La disciplina di tenere questi disaccoppiati ripaga la prima volta che qualcosa va storto. Se extract e transform sono incollati insieme e l’API restituisce spazzatura, non puoi rieseguire il transform sui dati di ieri senza colpire di nuovo l’API. Se sono disaccoppiati (extract scarica raw su disco, transform legge da disco) puoi rieseguire i transform cento volte senza mai toccare la sorgente. Questa è la singola leva di design più importante nel lavoro di pipeline e la maggior parte dei principianti la ignora.
def run_pipeline(date: str) -> None:
raw_path = extract(date) # writes raw/2026-04-01.json
clean_path = transform(raw_path) # writes clean/2026-04-01.parquet
load(clean_path) # upserts to warehouse
Tre funzioni. Ognuna è indipendentemente testabile, ripetibile e saltabile. Quello scaffold di quattro righe è lo scheletro di ogni pipeline che abbia mai messo in produzione.
Extract: gestire la sorgente
Le sorgenti vengono in circa quattro sapori e ognuno ha le sue insidie.
Database (Postgres, MySQL, l’OLTP della tua azienda). L’autenticazione è diretta; la trappola è il carico sulla sorgente. Un ingenuo SELECT * FROM orders contro un DB di produzione affollato ti procurerà un cordiale messaggio Slack. Usa una read replica se disponibile, preferisci query incrementali con un watermark, ed evita di girare nelle ore lavorative se puoi.
API (REST, GraphQL, SOAP se sei sfortunato). I rate limit sono la battaglia costante: vedi la lezione 39 per il toolkit completo retry-and-backoff. La paginazione c’è sempre e ne esistono circa quattro varianti. L’autenticazione spazia da “API key in header” a “OAuth dance con refresh token”. La maggior parte delle API mente su qualcosa nei loro documenti.
File (CSV in S3, Parquet in un data lake, drop di vendor su FTP). Facili da estrarre, difficili da capire quando sono “pronti”: vedi la lezione 38 per il pattern del lockfile. Lo schema drift è il killer silenzioso: appare una colonna nuova, cambia un formato di data, e la tua pipeline continua a girare ma produce assurdità.
Stream (Kafka, Kinesis, change-data-capture). Una bestia diversa: continua invece che batch, con le sue primitive (offset, consumer group, replay). Fuori scope per questo modulo, ma vale la pena sapere che esistono.
La decisione chiave dell’extract è incrementale o full. Full è più semplice: ad ogni run, tira tutto. Funziona finché “tutto” non è un miliardo di righe. L’incrementale è più difficile: tracci un watermark (un timestamp, un ID, un offset Kafka) e tiri solo ciò che è cambiato dall’ultima volta. Il prezzo che paghi è la pressione di correttezza: se la logica del watermark ha un bug, perdi dati silenziosamente, e il bug può nascondersi per mesi.
Un pattern sicuro è incrementale con full refresh periodici. Incrementali giornalieri, full reload settimanale. Il full reload recupera tutto ciò che gli incrementali hanno perso, e gli incrementali tengono basso il costo giornaliero.
Transform: l’ideale della funzione pura
Lo stadio del transform è dove Python (o SQL: tra un minuto) si guadagna la pagnotta. Il lavoro è un mix di:
- Cleaning: scartare righe spazzatura, sistemare i tipi, normalizzare le stringhe, gestire i null.
- Normalizzazione: dividere blob denormalizzati in tabelle relazionali appropriate, o viceversa.
- Join: arricchire una sorgente con un’altra (ordini + clienti, eventi + sessioni).
- Aggregazione: arrotolare a qualunque granularità i consumatori vogliano.
La disciplina che separa i transform buoni da quelli cattivi: rendili funzioni pure. Stesso input, stesso output. Niente letture dalla rete a metà transform. Niente datetime.now() che decide quale ramo prendere. Niente stato globale mutabile.
Perché? Perché i transform puri sono ripetibili. Se un report a valle è sbagliato, puoi rieseguire il transform di ieri sui dati raw di ieri e ottenere esattamente la stessa risposta sbagliata, poi sistemare il codice, rieseguire, e ottenere quella giusta. Se il transform legge dall’API a metà run, non puoi: l’API è andata avanti.
from pathlib import Path
import polars as pl
def transform(raw_path: Path) -> Path:
df = pl.read_json(raw_path)
df = (
df.filter(pl.col("status") != "deleted")
.with_columns(pl.col("amount").cast(pl.Float64))
.with_columns(pl.col("created_at").str.to_datetime())
.drop_nulls(subset=["customer_id"])
)
out = raw_path.parent.parent / "clean" / raw_path.with_suffix(".parquet").name
out.parent.mkdir(parents=True, exist_ok=True)
df.write_parquet(out)
return out
Quella funzione prende un path e restituisce un path. Nessun side effect oltre alla scrittura dell’output. Puoi chiamarla mille volte e fa la stessa cosa. È la proprietà da proteggere.
Load: write mode e idempotenza
Tre write mode che incontrerai:
- Append: attacca le righe nuove alla fine. Semplice, ma ririunning produce duplicati.
- Overwrite: spazza via la destinazione e scrivi fresco. Sicuro da rieseguire, ma costoso a scala, e rompe le query a valle mentre gira.
- Upsert (insert-or-update su una key): il gold standard per l’idempotenza. Riesegui gli stessi dati, ottieni lo stesso risultato.
Idempotency è la parola magica. Una pipeline è idempotente quando eseguirla due volte con lo stesso input produce lo stesso stato finale. Senza, ogni retry è un tiro di dadi; con, i retry sono gratis. La strada per l’idempotenza è quasi sempre “usa una key, fai upsert su di essa”. In Postgres è INSERT ... ON CONFLICT DO UPDATE. In Snowflake è MERGE. In una tabella Delta o Iceberg è MERGE INTO. Useremo il sapore Postgres nella lezione 38.
ELT: il riordino moderno
Nell’ultimo decennio i warehouse sono diventati molto più grandi e molto più economici. Snowflake, BigQuery, Databricks, DuckDB sul lato piccolo: tutti ti permettono di archiviare quantità enormi di dati raw e far girare transform SQL su di essi a basso costo. Così l’industria ha silenziosamente scambiato due lettere.
ELT = Extract, Load, Transform. Scarichi i dati raw nel warehouse per primi, poi trasformi con SQL dentro il warehouse. Il motore di trasformazione è il warehouse, non il tuo script Python.
Il vantaggio: il warehouse è costruito per questo. I join attraverso centinaia di milioni di righe sono veloci. I transform sono versionati in uno strumento come dbt, girano su schedule e producono documentazione gratis. Python diventa uno strato più sottile che fa per lo più solo extract-and-load.
Quando scegliere quale:
- ELT quando la tua destinazione è un vero warehouse e i tuoi transform hanno per lo più forma SQL (filtri, join, aggregati). Questo è il default per il lavoro di analytics nel 2026.
- ETL quando i transform richiedono Python vero: chiamare modelli ML, parsare formati strani, applicare logica business complessa che non si traduce in SQL. O quando la destinazione non è un warehouse (un’API, un search index, un servizio a valle).
In pratica fai entrambi. Il job ETL puramente Python e il modello ELT in stile dbt convivono felicemente nella stessa azienda.
Architettura medallion
Il buzzword che Databricks ha coniato e che il resto dell’industria ha adottato: livelli bronze, silver, gold.
- Bronze: raw, esattamente come ingerito. Schema-on-read, fedeltà totale, append-only, conservato per sempre (o per quanto la compliance ti permette). Se qualcosa a valle è sbagliato, il livello bronze è la fonte di verità da cui rieseguire.
- Silver: pulito e conforme. Nomi di colonna standard, tipi sistemati, deduplicato, joinato con dati di riferimento. Ancora a livello di riga: un evento-cliente per riga.
- Gold: pronto per l’analytics. Aggregato, denormalizzato, organizzato attorno a domande di business. Le tabelle che una dashboard BI legge.
Non ti serve Databricks per usarlo. Il pattern funziona altrettanto bene con tre schema Postgres, o tre prefissi S3, o tre sottodirectory su un server. Il punto è la stratificazione: ogni transform legge da un livello e scrive sul successivo, e ogni livello ha un contratto chiaro.
Lakehouse e formati di tabella
Il terzo pezzo di vocabolario è il lakehouse: un data lake (storage oggetto economico tipo S3) con transazioni ACID bullonate sopra tramite un formato di tabella aperto. Tre formati competono: Delta Lake (guidato da Databricks), Apache Iceberg (guidato da Netflix, ora lo standard aperto preferito dall’industria) e Apache Hudi (guidato da Uber).
Cosa ti danno: un layout file-Parquet-su-S3 che supporta update transazionali, delete, time travel (“mostrami questa tabella com’era martedì scorso”) ed evoluzione dello schema. Ottieni semantica simil-warehouse su storage economico da lake. Nel 2026 Iceberg ha lo slancio più forte: Snowflake, BigQuery, DuckDB e Trino lo leggono nativamente.
Non ti serve usare niente di tutto ciò al primo giorno. Dovresti sapere che esiste così che quando un architetto dice “atterriamo bronze in Iceberg” annuisci sapientemente.
Le decisioni che fai a ogni pipeline
Ogni volta che ti siedi a costruirne una, le stesse manciate di scelte tornano:
- Python o SQL? Usa SQL se il warehouse è la destinazione e la logica ha forma di set. Python per tutto il resto.
- Schedulato o event-triggered? Stile cron per batch prevedibili; event-triggered (file-arrival, webhook, messaggio in coda) per sorgenti a bassa latenza o irregolari.
- Leggere una volta o incrementalmente? Full è più semplice; incrementale scala. Il watermark con cura.
- Qual è la storia di recovery? Se questo fallisce a metà, come si presenta la riesecuzione? Se non puoi rispondere, non hai ancora una pipeline: hai uno script che ha funzionato una volta.
I pezzi di una pipeline reale
Una pipeline di produzione non è solo tre funzioni; è un ecosistema.
- Orchestrator: la cosa che fa girare i job nell’ordine giusto sullo schedule giusto. Cron per la v1, Airflow / Prefect / Dagster quando le dipendenze crescono. La lezione 41 lo copre.
- Trasformazioni: il tuo Python e/o SQL.
- Sink: destinazioni. Spesso più di una (warehouse + cache + search index).
- Monitoring: il job sta girando? Per quanto? Quanti dati?
- Alerting: chiama qualcuno quando si rompe alle 3 di notte.
- Lineage / catalog: cosa alimenta cosa, chi è il proprietario, da dove viene questa colonna?
La maggior parte dei team comincia con orchestrator + trasformazioni e bullona il resto man mano che il blast radius dei fallimenti cresce. Non c’è vergogna in questo. C’è vergogna nel non bullonarli quando servono.
Da qui in poi
Nella lezione 38 costruiamo una pipeline di ingestion vera: i file arrivano in una cartella, uno script Python li raccoglie, valida, carica su Postgres, con tutti i pattern che sopravvivono al contatto con la realtà. Nella lezione 39 ci spostiamo sul lato extract via API: retry, rate limit, paginazione. Per la lezione 41 abbiamo un orchestrator avvolto attorno al tutto, e hai costruito qualcosa che non sfigurerebbe in un negozio reale.
Per ora, il modello mentale: estrai dal mondo esterno caotico, trasforma in puro Python o SQL, carica idempotentemente sulla destinazione, stratifica le tue tabelle, e progetta ogni passo così che le riesecuzioni siano gratis. Questo è ETL. Il resto sono dettagli.
Citazioni
- Documentazione Apache Iceberg: https://iceberg.apache.org/docs/, recuperata il 2026-05-01.
- Documentazione Delta Lake: https://docs.delta.io/, recuperata il 2026-05-01.
- Architettura medallion Databricks: https://www.databricks.com/glossary/medallion-architecture, recuperata il 2026-05-01.