Microsoft Fabric Lakehouse brings together the best of data lakes and data warehouses. Combined with PySpark, you can build robust ETL pipelines that read from virtually any source and land clean, typed data into Delta Lake tables. This guide walks through the entire process.
Prerequisites
- A Microsoft Fabric workspace with a Lakehouse created
- Access to a Fabric Notebook (or local PySpark environment with Fabric SDK)
- Source files: CSV, JSON, or Parquet (or API endpoints returning JSON)
1. Reading CSV files
PySpark handles CSV natively with schema inference and configurable options for headers, delimiters, and null handling.
from pyspark.sql import SparkSession
spark = SparkSession.builder.getOrCreate()
df_csv = (spark.read
.option("header", "true")
.option("inferSchema", "true")
.option("nullValue", "")
.csv("Files/raw/customers.csv"))
df_csv.printSchema()
df_csv.show(5)
For production workloads, define the schema explicitly instead of relying on inference — it's faster and prevents type mismatches across file batches.
2. Reading JSON files
JSON files, including nested structures, are handled naturally by Spark's DataFrame API.
df_json = (spark.read
.option("multiline", "true")
.json("Files/raw/orders.json"))
# Flatten nested structs
from pyspark.sql.functions import col
df_flat = df_json.select(
col("order_id"),
col("customer.name").alias("customer_name"),
col("customer.email").alias("customer_email"),
col("amount"),
col("created_at")
)
3. Reading Parquet files
Parquet is the most efficient format for Lakehouse workloads — it preserves schema, supports predicate pushdown, and compresses well.
df_parquet = spark.read.parquet("Files/raw/events/")
# Parquet retains types, so no inference needed
print(f"Rows: {df_parquet.count()}")
print(f"Columns: {len(df_parquet.columns)}")
4. Reading from REST APIs
For API sources, fetch the data with requests and convert to a DataFrame.
import requests
import json
response = requests.get("https://api.example.com/v1/products",
headers={"Authorization": "Bearer YOUR_TOKEN"})
data = response.json()["results"]
df_api = spark.createDataFrame(data)
5. Writing to Fabric Lakehouse (Delta Lake)
The key step — writing DataFrames as Delta tables inside the Lakehouse. Fabric manages the metastore automatically.
# Write as a managed Delta table
df_csv.write \
.mode("overwrite") \
.format("delta") \
.saveAsTable("lakehouse.customers")
# Append new data
df_json.write \
.mode("append") \
.format("delta") \
.saveAsTable("lakehouse.orders")
Use
mode("overwrite")for full refreshes andmode("append")for incremental loads. For upserts, use Delta Lake'sMERGE INTOoperation.
6. Schema evolution
When source schemas change over time, enable automatic schema merging:
df_new.write \
.mode("append") \
.option("mergeSchema", "true") \
.format("delta") \
.saveAsTable("lakehouse.customers")
7. Putting it all together
Here's a complete pipeline pattern that reads from multiple sources and writes to a single Lakehouse table:
from pyspark.sql.functions import current_timestamp, lit
def ingest_to_lakehouse(df, table_name, source_name, mode="append"):
"""Standard ingestion with audit columns."""
df_enriched = (df
.withColumn("_ingested_at", current_timestamp())
.withColumn("_source", lit(source_name)))
df_enriched.write \
.mode(mode) \
.option("mergeSchema", "true") \
.format("delta") \
.saveAsTable(f"lakehouse.{table_name}")
print(f"✅ {df.count()} rows → {table_name} from {source_name}")
# Run
ingest_to_lakehouse(df_csv, "customers", "csv_upload")
ingest_to_lakehouse(df_api, "products", "rest_api")
ingest_to_lakehouse(df_parquet, "events", "parquet_archive")
Key takeaways
- PySpark + Fabric Lakehouse handles CSV, JSON, Parquet, and API sources uniformly
- Always define explicit schemas for production pipelines
- Use Delta Lake's ACID transactions and schema evolution for reliable ingestion
- Add audit columns (
_ingested_at,_source) to track data lineage - Use DataDevKIT's Data Profiler to inspect your source files before building the pipeline