Reference: A Data Platform / ETL System
A reference ETL layout for Python data teams: ingest from APIs and files, transform with Polars 1.x or pandas 2.2+, load to Snowflake/BigQuery/Postgres, orchestrated by Prefect or Airflow with idempotent stages and freshness SLAs.
Recipe
Quick-reference recipe card - copy-paste ready.
data-platform/
flows/ingest_orders.py # orchestrated flow
transforms/orders.py # pure functions
loaders/warehouse.py # COPY / MERGE
contracts/orders.yaml # schema + SLAs
tests/test_transforms.pyWhen to reach for this:
- Building analytics platform on Python
- Replacing cron spaghetti scripts
- RFC for data mesh boundary
Working Example
"""transforms/orders.py - idempotent transform."""
from __future__ import annotations
import polars as pl
def normalize_orders(raw: pl.LazyFrame) -> pl.LazyFrame:
return (
raw.filter(pl.col("status").is_in(["paid", "refunded"]))
.with_columns(
pl.col("amount").cast(pl.Float64),
pl.col("created_at").str.to_datetime(strict=False),
)
.select("order_id", "tenant_id", "amount", "status", "created_at")
)"""flows/ingest_orders.py - Prefect-style flow sketch."""
from __future__ import annotations
from datetime import datetime, timezone
import polars as pl
from loaders.warehouse import merge_into_warehouse
from transforms.orders import normalize_orders
WATERMARK_KEY = "orders_ingest"
def run_ingest(since: datetime) -> int:
raw_path = f"s3://lake/raw/orders/{since.date()}.parquet"
raw = pl.scan_parquet(raw_path)
clean = normalize_orders(raw).collect()
rows = merge_into_warehouse(clean, table="analytics.orders", key="order_id")
return rows
if __name__ == "__main__":
ts = datetime.now(timezone.utc)
print(f"loaded {run_ingest(ts)} rows")# contracts/orders.yaml
name: orders
freshness_sla_hours: 6
columns:
order_id: { type: string, required: true }
amount: { type: float, min: 0 }What this demonstrates:
- Transforms are pure functions tested without orchestrator
- Lazy Polars for memory-efficient scans
- MERGE load keyed for idempotency
- Data contract documents SLA and schema
Deep Dive
How It Works
- Watermark - Store high-water mark; re-run safe for same window.
- Orchestration - Retries, alerting, dependency graph (dims before facts).
- Lake + warehouse - Raw immutable; curated tables in warehouse.
- Quality checks - Row count bounds, null rates, contract validation.
- Lineage - Document source systems per table in catalog.
Layer Model
| Layer | Content |
|---|---|
| Raw | API dumps, CDC events |
| Staging | Typed, deduped |
| Mart | Business aggregates |
| Export | ML features, reverse ETL |
Python Notes
# loaders/warehouse.py sketch
def merge_into_warehouse(df: pl.DataFrame, table: str, key: str) -> int:
# write parquet staging → MERGE SQL
return df.heightGotchas
- Non-idempotent append - Duplicate rows on retry. Fix: MERGE on natural key.
- Giant pandas in memory - OOM on worker. Fix: Polars lazy scan or chunked read.
- Cron without observability - Silent stale data. Fix: Freshness alert on
max(created_at). - Schema change without contract - Downstream ML breaks. Fix: Version contract YAML in PR.
- Sharing DB creds in notebooks - Leak risk. Fix: Orchestrator secret injection only.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| dbt for SQL transforms | Warehouse-native team | Heavy Python ML features in flight |
| Spark (PySpark) | TB scale | GB daily volume |
| Streaming (Kafka+Flink) | Real-time SLAs | Hourly batch enough |
| Vendor ELT (Fivetran) | SaaS sources | Custom API logic heavy |
FAQs
Airflow vs Prefect?
Both fine; Prefect lighter for Python-native teams; Airflow when existing ops investment.
pandas vs Polars?
Polars default for new batch; pandas when ecosystem requires specific lib.
Where run jobs?
K8s CronJob or managed workflow; not on analyst laptops.
Backfill strategy?
Partition by date; idempotent MERGE; throttle warehouse slots.
PII handling?
Tokenize in staging; role-based access in warehouse; log access.
Testing?
pytest on transforms with fixture parquet; contract test on column set.
Failed partial load?
Transactional MERGE or staging swap; never half-updated mart without marker.
Cost control?
Partition pruning, column projection, schedule off-peak, autoscale workers down.
Reverse ETL?
Python job pushes aggregates to Postgres for app - document separate SLA.
Data mesh?
Domain owns marts; platform owns orchestration template and contracts.
Related
- Reference: A Production ML Pipeline - downstream ML
- dbt with Python - SQL layer
- Data Pipelines - orchestration patterns
- Polars Basics - transform engine
- Lessons-Learned Catalog - common mistakes
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.