Wicked Smart Data
LearnInsightsAboutContact
Sign InLet's Build
LearnInsightsAboutContact
Sign InLet's Build
Wicked Smart Data

Intelligence, automation, and expert execution — plus an elite library of free knowledge. We turn complexity into competitive advantage.

Start a conversation

Platform

  • Learning Paths
  • Insights
  • RSS Feed

Company

  • About
  • Contact
  • Work With Us

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Wicked Smart Data. All rights reserved.

Intelligence · Automation · Advantage

All Insights
Microsoft Fabric

Writing and Running Your First PySpark Notebook in Microsoft Fabric: Reading Files, Creating Delta Tables, and Saving to the Lakehouse

Learn how to build a real PySpark notebook in Microsoft Fabric from scratch — reading CSV files from Lakehouse storage, cleaning and transforming data, and writing Delta tables that are immediately queryable via SQL. This hands-on lesson teaches Spark fundamentals in the context you'll actually use them.

🌱 Foundation17 min readSep 22, 2026Updated Sep 22, 2026
Writing and Running Your First PySpark Notebook in Microsoft Fabric: Reading Files, Creating Delta Tables, and Saving to the Lakehouse
On this page
  • Introduction
  • Prerequisites
  • What Is PySpark, and Why Does It Matter Here?
  • Setting Up Your Notebook
  • Creating the Notebook
  • Understanding the Notebook Interface
  • Uploading a File to the Lakehouse Files Section
  • Reading a CSV File with PySpark
  • Transforming the Data
  • Writing a Delta Table to the Lakehouse
  • Querying Your Delta Table with SQL
  • Verifying via the SQL Analytics Endpoint
Hands-On Exercise
  • Common Mistakes & Troubleshooting
  • "Path does not exist" error when reading files
  • Session takes forever to start
  • `inferSchema` gives wrong types
  • Table doesn't appear in the left panel after writing
  • `AnalysisException: Table already exists`
  • Spark job runs but produces empty output
  • Summary & Next Steps
  • Writing and Running Your First PySpark Notebook in Microsoft Fabric: Reading Files, Creating Delta Tables, and Saving to the Lakehouse

    Introduction

    Imagine you've just landed a raw CSV export from your company's sales system — 500,000 rows, no data types enforced, column names like Amt and Dt that only the person who wrote the export knows the meaning of. You need to clean it up, give it proper structure, and land it somewhere your analysts can query reliably. A Dataflow Gen2 might get you part of the way there, but the moment you need to write conditional logic, loop over nested structures, or run complex transformations that go beyond Power Query's click-and-point interface, you want a notebook.

    Microsoft Fabric's Spark notebooks give you a full Python environment — with PySpark, pandas, and dozens of other libraries — running on a distributed compute engine, connected directly to your Lakehouse. That last part is what makes Fabric notebooks different from a regular Jupyter notebook: the integration is native. You can read a file from your Lakehouse's Files section with a single line of code, transform it, and write it as a Delta table that immediately shows up in the Tables section — no connection strings, no credentials, no upload step. It just works.

    By the end of this lesson, you'll have run a real PySpark notebook from scratch. You'll understand how Spark reads data, how DataFrames work, and how to write a Delta table that lives permanently in your Lakehouse and powers downstream reporting.

    What you'll learn:

    • How to create and navigate a Spark notebook in Microsoft Fabric
    • How to read CSV and Parquet files from the Lakehouse Files section using PySpark
    • How to inspect, filter, and transform data using a Spark DataFrame
    • How to write a cleaned DataFrame as a Delta table in the Lakehouse Tables section
    • How to verify your table is queryable via the SQL Analytics Endpoint

    Prerequisites

    Before you start this lesson, you should have:

    • Access to a Microsoft Fabric workspace with at least a Trial or F2 capacity (see Fabric Capacities and Workspaces: F SKUs, Trials, and Setting Up Your First Workspace if you need to get set up)
    • A Lakehouse already created in your workspace (if you haven't done this yet, Building Your First Lakehouse in Microsoft Fabric: Files, Tables, and the SQL Analytics Endpoint walks you through it)
    • A basic comfort level with Python — you don't need to be an expert, but knowing what a variable and a function are will help

    You do not need any prior Spark experience. We'll explain everything as we go.


    What Is PySpark, and Why Does It Matter Here?

    Apache Spark is a distributed computing engine — meaning it can split a large dataset across many machines and process each chunk in parallel. PySpark is the Python API for Spark. When you write PySpark code, you're writing Python that sends instructions to the Spark engine, which then handles the actual heavy lifting.

    In Microsoft Fabric, every notebook runs on a Spark cluster that Fabric manages for you. When you run your first cell, Fabric spins up that cluster (this is called the session startup, and it takes 30–60 seconds the first time). After that, your code executes against the live cluster until you close the session.

    The central object you'll work with is the DataFrame — think of it like a table with named columns and typed data, similar to a pandas DataFrame or a SQL table. Unlike a pandas DataFrame, a Spark DataFrame is lazy: when you write a transformation, Spark doesn't actually execute it immediately. It builds up a plan and runs everything when you explicitly ask for results. This is important to understand because it means you can chain many transformations together and Spark will optimize the whole plan at once.

    Key insight

    Spark DataFrames are lazy. Calling .filter() or .select() doesn't run any computation — it just refines the plan. Computation happens when you call an action like .show(), .count(), or .write. This is why your first transformation often feels instant, but the next .show() takes a few seconds.


    Setting Up Your Notebook

    Creating the Notebook

    In your Fabric workspace, navigate to your Lakehouse item and open it. Inside the Lakehouse editor, you'll see two sections in the left-hand explorer panel: Files and Tables. At the top of the screen, look for the Open notebook button or use the New dropdown and select Notebook. This creates a new notebook and attaches it to your Lakehouse automatically — that attachment is what gives you the seamless file access you're about to use.

    Alternatively, go to your workspace home, click New item, and choose Notebook. If you create a notebook this way, you'll need to attach your Lakehouse manually: in the left panel of the notebook, click Add lakehouse and select the one you created.

    Give your notebook a meaningful name. Click the default name at the top (usually "Notebook 1") and rename it to something like sales_ingest_notebook. This matters more than it seems — when you have a dozen notebooks in a workspace, vague names become painful.

    Understanding the Notebook Interface

    The notebook interface consists of:

    • A toolbar at the top with run controls and language options
    • A left panel showing attached Lakehouses and their Files/Tables structure
    • Cells in the main area, where you write and run code

    Each cell can be either Code (PySpark, Python, SQL, or Scala) or Markdown (for documentation). By default, cells run Python/PySpark. You can add cells using the + Code or + Markdown buttons that appear between cells when you hover.

    Tip

    Get into the habit of using Markdown cells between your code cells to document what each block does. A notebook you wrote three months ago with no comments is nearly unreadable. Treat it like a report you'll hand to a colleague.


    Uploading a File to the Lakehouse Files Section

    Before we can read anything, we need something to read. For this lesson, we'll use a realistic sales CSV. In a real project, this file might arrive via a pipeline or a shortcut to blob storage — but for learning purposes, we'll upload it manually.

    Create a file on your computer called sales_2024.csv with this content:

    order_id,customer_id,product_name,quantity,unit_price,order_date,region
    1001,C441,Wireless Keyboard,2,49.99,2024-01-15,North
    1002,C882,USB-C Hub,1,34.99,2024-01-15,South
    1003,C441,Monitor Stand,1,79.99,2024-01-16,North
    1004,C119,Wireless Keyboard,3,49.99,2024-01-17,West
    1005,C882,Laptop Bag,2,59.99,2024-01-18,South
    1006,C330,USB-C Hub,4,34.99,2024-01-20,East
    1007,C119,Monitor Stand,2,79.99,2024-01-22,West
    1008,C441,Laptop Bag,1,59.99,2024-01-23,North
    

    In the Lakehouse explorer (the left panel of either the Lakehouse editor or your notebook), right-click on Files and choose Upload → Upload files. Select your sales_2024.csv file. Within a few seconds, it appears under Files. You can expand the Files section to confirm it's there.

    Note

    The Files section of a Lakehouse is raw object storage — it's part of OneLake, Microsoft's unified storage layer. Files here are not queryable by SQL automatically; they're just files. The Tables section is where Delta tables live, and those are queryable. Our job in this notebook is to promote a file into a table.


    Reading a CSV File with PySpark

    Now let's write some code. Click into the first cell of your notebook and type the following:

    # Read the CSV file from the Lakehouse Files section
    df_raw = spark.read.option("header", True).option("inferSchema", True).csv("Files/sales_2024.csv")
    
    # Show the first 10 rows
    df_raw.show(10)
    

    Click the Run cell button (the triangle/play icon on the left of the cell) or press Shift+Enter.

    The first time you run any cell in a new session, you'll see a message like "Starting Spark session..." — this is normal. After 30–60 seconds, the session starts and your code runs. You'll see output like a formatted table with your 8 rows.

    Let's break down what that spark.read line does:

    • spark is the SparkSession object — your entry point to all Spark functionality. Fabric creates and injects this automatically; you never have to initialize it yourself.
    • .read gives you a DataFrameReader object
    • .option("header", True) tells Spark to use the first row as column names
    • .option("inferSchema", True) tells Spark to look at the data and guess the right data types (integer, string, double, etc.) rather than making everything a string
    • .csv("Files/sales_2024.csv") specifies the path — relative to the root of your attached Lakehouse

    The path "Files/sales_2024.csv" is the key piece of magic here. When a Lakehouse is attached to a notebook, Fabric maps the Lakehouse's OneLake path so that "Files/" refers to the Files section and "Tables/" refers to the Tables section. You never need to write a full ABFS path like abfss://....

    Now add a second cell to inspect the schema:

    # Inspect the inferred schema
    df_raw.printSchema()
    

    You should see something like:

    root
     |-- order_id: integer (nullable = true)
     |-- customer_id: string (nullable = true)
     |-- product_name: string (nullable = true)
     |-- quantity: integer (nullable = true)
     |-- unit_price: double (nullable = true)
     |-- order_date: string (nullable = true)
     |-- region: string (nullable = true)
    

    Notice that order_date was read as a string, not a date. inferSchema is convenient but imperfect — it rarely guesses dates correctly. We'll fix this in the next section.

    Warning

    Avoid using inferSchema on large production files. Spark has to make a full pass through the data to infer types, which doubles your read time. On files with millions of rows, define the schema explicitly using a StructType. For this tutorial, inferSchema is fine.


    Transforming the Data

    Real data work always involves cleaning and shaping. Let's do three things: cast the date column to a proper date type, calculate a total_amount column, and rename a column to be more descriptive.

    Add a new code cell:

    from pyspark.sql.functions import col, to_date, round as spark_round
    
    df_clean = (
        df_raw
        # Cast order_date from string to a proper DateType
        .withColumn("order_date", to_date(col("order_date"), "yyyy-MM-dd"))
        # Add a calculated column: total sale amount
        .withColumn("total_amount", spark_round(col("quantity") * col("unit_price"), 2))
        # Rename customer_id to be more explicit
        .withColumnRenamed("customer_id", "customer_code")
        # Drop rows where order_id is null (defensive cleaning)
        .filter(col("order_id").isNotNull())
    )
    
    df_clean.show(10)
    

    Let's walk through what's happening:

    • withColumn("order_date", to_date(...)) replaces the order_date column with a properly typed date. to_date parses a string using the format you provide — "yyyy-MM-dd" matches our data's format like 2024-01-15.
    • withColumn("total_amount", ...) creates a brand new column. We multiply quantity by unit price and round to 2 decimal places.
    • withColumnRenamed renames a column without touching its data.
    • filter(col("order_id").isNotNull()) removes any rows where order_id is missing — a basic but important defensive step.

    Notice how we chain all these operations together inside parentheses. This is idiomatic PySpark: each transformation returns a new DataFrame (Spark DataFrames are immutable — you never modify one in place), so you chain them fluently. The parentheses just let us break the chain across multiple lines for readability.

    Run this cell. You should see your cleaned DataFrame with the new total_amount column and properly formatted dates.


    Writing a Delta Table to the Lakehouse

    Here's where things get powerful. A Delta table is a Parquet-based file format with an added transaction log — that log is what enables ACID transactions, time travel, and schema enforcement. When you save to Delta format in a Fabric Lakehouse, the table immediately becomes queryable via the SQL Analytics Endpoint without any additional configuration. This is the foundation of how Direct Lake mode in Power BI works — it reads Delta files directly from OneLake at query time.

    Add a new cell to write your cleaned DataFrame as a Delta table:

    # Write the cleaned DataFrame as a Delta table in the Lakehouse Tables section
    (
        df_clean
        .write
        .format("delta")
        .mode("overwrite")
        .option("overwriteSchema", True)
        .saveAsTable("sales_orders_clean")
    )
    
    print("Table written successfully.")
    

    Run this cell. It will take a few seconds. When it completes, look at the left panel of your notebook — expand the Tables section under your Lakehouse. You should see sales_orders_clean listed there. If it doesn't appear immediately, click the refresh icon next to Tables.

    Let's break down the write options:

    • .format("delta") — write as Delta format, not plain Parquet or CSV
    • .mode("overwrite") — if the table already exists, replace it. Other options are "append" (add rows), "ignore" (skip if exists), and "error" (the default, which throws an error if the table exists)
    • .option("overwriteSchema", True) — allows overwriting the table even if the schema has changed. Useful during development when you're iterating on column definitions
    • .saveAsTable("sales_orders_clean") — registers the table in the Lakehouse's metastore with this name, which is how it becomes discoverable

    Tip

    Use saveAsTable("name") rather than .save("Tables/name") when working in Fabric notebooks. Both write to the same physical location, but saveAsTable also registers the table in the metastore, making it immediately queryable via SQL and visible in the Lakehouse explorer. Using .save() writes the files but doesn't register the table — you'd have to register it manually.


    Querying Your Delta Table with SQL

    One of the nicest features of Fabric notebooks is that you can switch languages per cell. Add a new cell, and at the top of the cell, change the language to SparkSQL by clicking the language dropdown on the right side of the cell and selecting SparkSQL (or just type %%sql as the very first line of the cell).

    %%sql
    
    SELECT 
        region,
        COUNT(*) AS order_count,
        SUM(total_amount) AS total_revenue,
        ROUND(AVG(total_amount), 2) AS avg_order_value
    FROM sales_orders_clean
    GROUP BY region
    ORDER BY total_revenue DESC
    

    Run this cell. You'll get a formatted result table showing revenue by region — directly querying the Delta table you just created. No connection string. No export. It's just there.

    This is the real value of the Lakehouse model: you write data once, and it's immediately accessible to SQL queries, notebooks, and Power BI reports.


    Verifying via the SQL Analytics Endpoint

    Every Fabric Lakehouse comes with a SQL Analytics Endpoint — a read-only SQL interface that lets you query your Delta tables using T-SQL from any SQL client or from Power BI. To verify your table is there:

    1. Go back to your workspace home (click the workspace name in the breadcrumb at the top of the page)
    2. Find your Lakehouse item — you'll notice it shows as two items: the Lakehouse itself and a "SQL analytics endpoint" item
    3. Click the SQL Analytics Endpoint item
    4. In the SQL editor that opens, run: SELECT TOP 5 * FROM sales_orders_clean

    If you see your data, everything worked. Your notebook wrote a Delta table that is now queryable from a T-SQL interface without any extra work. This is exactly how the Lakehouse fits into the broader Fabric architecture — it gives you the flexibility of file storage with the queryability of a warehouse.


    Hands-On Exercise

    Now it's your turn to extend what you've built. Complete the following tasks using what you've learned:

    1. Add a new transformation: In your df_clean pipeline, add a withColumn step that creates a column called price_tier. Use pyspark.sql.functions.when to label orders as "Budget" if unit_price < 40, "Mid-range" if between 40 and 70, and "Premium" if above 70. (Hint: look up when().when().otherwise() in PySpark.)

    2. Filter to a specific region: Create a new DataFrame df_north that contains only orders from the "North" region. Use .filter().

    3. Write a second table: Write df_north as a Delta table named sales_orders_north using .mode("append") instead of "overwrite". Run the cell twice and check how many rows are in the table — this will demonstrate why "overwrite" vs "append" matters.

    4. Query with SQL: Write a %%sql cell that joins sales_orders_clean and sales_orders_north to verify they share the same schema.

    Key insight

    Completing exercise 3 intentionally twice is meant to teach you a lesson about "append" mode. After two runs, you'll have duplicate rows — which is what happens in real pipelines when they re-run without a deduplication strategy. This is one reason medallion architecture patterns deduplicate at the Silver layer. See Implementing the Medallion Architecture in Microsoft Fabric for how teams handle this at scale.


    Common Mistakes & Troubleshooting

    "Path does not exist" error when reading files

    This almost always means either the file name is wrong (check capitalization — OneLake paths are case-sensitive) or the Lakehouse isn't attached to the notebook. Open the left panel, look for your Lakehouse under "Lakehouses," and verify it shows the file you uploaded. If no Lakehouse is listed, click Add lakehouse.

    Session takes forever to start

    Fabric starts a Spark cluster fresh for each session. On Trial capacities or small F SKUs, this can take 60–90 seconds. This is normal. Once started, subsequent cells run much faster. If it's been more than 3 minutes, try refreshing the page and running again.

    `inferSchema` gives wrong types

    This is expected behavior, not a bug. inferSchema samples the data and makes educated guesses. For columns you know the type of (especially dates), always cast explicitly using to_date, cast("integer"), or similar functions after reading. On large files, define the full schema upfront using StructType.

    Table doesn't appear in the left panel after writing

    Click the refresh icon next to the Tables section in the left panel. The table is written to storage immediately, but the UI doesn't always auto-refresh. If it still doesn't appear after refreshing, verify the write cell completed without errors.

    `AnalysisException: Table already exists`

    This happens when you run a write cell with .mode("error") (the default) on a table that already exists. Add .mode("overwrite") to your write call, or drop the table first with spark.sql("DROP TABLE IF EXISTS sales_orders_clean").

    Warning

    Be careful with DROP TABLE in production environments. Dropping a managed Delta table also deletes the underlying Parquet files. If you only want to clear the registered table metadata without deleting data, use DROP TABLE on external tables only — or better yet, use mode("overwrite") in your write call so Spark handles the replacement safely.

    Spark job runs but produces empty output

    Check your filter conditions. A common mistake is filtering on a string column with the wrong case: filter(col("region") == "north") returns no rows if the data stores it as "North". Use .show() at intermediate steps to inspect the DataFrame before and after each transformation.


    Summary & Next Steps

    You've just written a complete PySpark notebook that reads a raw CSV file from Lakehouse storage, cleans and enriches the data with typed columns and calculated fields, and writes a queryable Delta table — all in under 20 lines of code. More importantly, you understand why each piece works: the SparkSession, lazy evaluation, the read/write API, and the relationship between the Files and Tables sections of a Lakehouse.

    Here's what you built:

    • A notebook attached to a Lakehouse, reading files via simple relative paths
    • A transformation pipeline using withColumn, to_date, withColumnRenamed, and filter
    • A Delta table written with saveAsTable, immediately queryable via SparkSQL and T-SQL
    • Awareness of write modes (overwrite vs append) and their implications

    Where to go from here:

    If you want to go deeper on what PySpark can do for complex data transformations — including joins, window functions, and working with nested JSON — Transforming Data with Spark Notebooks in Microsoft Fabric: PySpark for Lakehouse Tables is the natural next step.

    Once you're comfortable writing notebooks manually, you'll want to automate them — running on a schedule or triggered by upstream pipeline events. Orchestrating Loads with Fabric Data Pipelines covers how to call a notebook from a pipeline and pass parameters into it.

    And if your data is landing in the Lakehouse from external cloud sources like Azure Data Lake Storage or S3 — and you want to avoid copying it — read about OneLake shortcuts, which let your notebook read remote data as if it were local.

    Work With Us

    From insight to implementation

    Reading is the start. When you're ready to build the data, automation, or AI systems behind it, our team turns strategy into shipped results.

    Let's Build

    Microsoft Fabric Fundamentals

    Previous

    Scheduling and Automating Fabric Data Pipeline Runs with Activity-Level Retries, Alerts, and Email Notifications

    Next

    Connecting a Power BI Semantic Model to a Fabric Lakehouse in Direct Lake Mode: Creating, Refreshing, and Optimizing Delta Tables for Reporting

    Related Insights

    Microsoft FabricFoundation

    Writing Your First PySpark Notebook in Microsoft Fabric: Reading CSV Files from OneLake, Exploring Data with DataFrames, and Saving Results as a Delta Table

    16 min
    Microsoft FabricFoundation

    Writing Your First PySpark Notebook in Microsoft Fabric: Reading CSV Files from OneLake, Transforming Data with DataFrames, and Writing Delta Tables to a Lakehouse

    17 min
    Microsoft FabricFoundation

    Writing Your First PySpark Notebook in Microsoft Fabric: Reading CSV Files from OneLake, Transforming Data with DataFrames, and Saving Results as a Delta Table

    14 min

    On this page

    • Introduction
    • Prerequisites
    • What Is PySpark, and Why Does It Matter Here?
    • Setting Up Your Notebook
    • Creating the Notebook
    • Understanding the Notebook Interface
    • Uploading a File to the Lakehouse Files Section
    • Reading a CSV File with PySpark
    • Transforming the Data
    • Writing a Delta Table to the Lakehouse
    • Querying Your Delta Table with SQL
    • Verifying via the SQL Analytics Endpoint
    • Hands-On Exercise
    • Common Mistakes & Troubleshooting
    • "Path does not exist" error when reading files
    • Session takes forever to start
    • `inferSchema` gives wrong types
    • Table doesn't appear in the left panel after writing
    • `AnalysisException: Table already exists`
    • Spark job runs but produces empty output
    • Summary & Next Steps