The best way to understand ETL is to build one. In this tutorial you will create a complete, working ETL pipeline using Python and SQL — from raw CSV data to a queryable SQLite database. Every step maps directly to the patterns used in production pipelines.
Prerequisites: Basic Python knowledge. No prior data engineering experience needed.
Setup
pip install pandas sqlalchemy
Project structure:
etl_project/
├── etl.py
└── data/
└── sales_data.csv
Step 1: Extract
Extraction pulls raw data from the source. Here the source is a CSV file — in production this would be a database, API, or cloud storage bucket. The pattern is identical.
import pandas as pd def extract(file_path: str) -> pd.DataFrame: df = pd.read_csv(file_path) print(f"[EXTRACT] {len(df)} records loaded from {file_path}") return df
Step 2: Transform
This is the most important stage — and where most ETL bugs hide. Always validate record counts before and after — unexpected drops show you exactly where data is being lost.
def transform(df: pd.DataFrame) -> pd.DataFrame: df = df.copy() # Drop rows missing critical fields df.dropna(subset=['order_id', 'customer_id'], inplace=True) # Fix data types — coerce bad values to NaN, then fill df['quantity'] = pd.to_numeric(df['quantity'], errors='coerce').fillna(0).astype(int) df['price'] = pd.to_numeric(df['price'], errors='coerce').fillna(0.0) # Standardize dates df['order_date'] = pd.to_datetime(df['order_date'], errors='coerce') df.dropna(subset=['order_date'], inplace=True) # Remove duplicates df.drop_duplicates(subset=['order_id'], inplace=True) # Calculate derived field df['total_revenue'] = df['quantity'] * df['price'] print(f"[TRANSFORM] {len(df)} clean records") return df
Step 3: Load
Use if_exists='replace' for full loads. Use if_exists='append' for incremental loads that add records to an existing table.
from sqlalchemy import create_engine def load(df: pd.DataFrame, table_name: str, db_path: str = "warehouse.db"): engine = create_engine(f"sqlite:///{db_path}") df.to_sql( name=table_name, con=engine, if_exists='replace', index=False ) print(f"[LOAD] {len(df)} records → '{table_name}' in {db_path}")
Step 4: Orchestrate
def run_etl(): raw_df = extract("data/sales_data.csv") clean_df = transform(raw_df) load(clean_df, table_name="fact_sales") if __name__ == "__main__": run_etl()
python etl.py # [EXTRACT] 5000 records loaded from data/sales_data.csv # [TRANSFORM] 4837 clean records # [LOAD] 4837 records → 'fact_sales' in warehouse.db
Step 5: Query the Warehouse
-- Total revenue by product SELECT product, SUM(total_revenue) AS revenue FROM fact_sales GROUP BY product ORDER BY revenue DESC; -- Top 10 customers SELECT customer_id, SUM(total_revenue) AS total_spend FROM fact_sales GROUP BY customer_id ORDER BY total_spend DESC LIMIT 10;
Next Steps
- Swap CSV for a real database using
pd.read_sql()+ SQLAlchemy connection string - Add logging with Python's
loggingmodule instead of print statements - Schedule it with cron or Apache Airflow
- Add tests using pytest to validate data at each stage — covered in the Python ETL Testing Workflow post
- Plan your career path — see what to learn next in our ETL developer course guide
Frequently Asked Questions
What Python libraries do I need to build an ETL pipeline?
What is the difference between if_exists replace and append in pandas to_sql?
How do I schedule a Python ETL pipeline?
Asim Noaman Lodhi
Certified Google Partner · QA Consultant · 12+ Years IT
QA consultant specializing in ETL testing and data quality. Trained 913+ students to transition into data testing roles through hands-on, real-world instruction.