Skip to content
Everyone knows 7 min read · Setup

Run Spark on your laptop

Every lesson in this track and the Lakehouse track ends with labs you run yourself. This page gets a real Spark session running on your laptop in a few minutes: plain Spark, Spark with Delta Lake, Iceberg or Hudi, and Spark Connect.

What you need

RequirementCheck withNotes
Java 17 or 21java -versionSpark runs in the JVM. On macOS: brew install openjdk@17; on Windows or Linux, any OpenJDK 17 build. Set JAVA_HOME if several are installed.
Python 3.10 to 3.13python3 --versionUse a virtual environment: python3 -m venv .venv, then source .venv/bin/activate (or .venv\Scripts\activate on Windows).
PySparkpip install pysparkThe pip package includes Spark itself: no separate download or cluster needed.

Run the labs in a Jupyter notebook, a Python script, or the pyspark shell. Each lab assumes a variable called spark, created by one of the sessions below, and writes its files under a lab/ folder in your working directory.

A plain Spark session

PySpark · Used by the Spark internals and PySpark labs
# pip install pyspark==4.2.0
from pyspark.sql import SparkSession

spark = (SparkSession.builder
    .master("local[4]")          # run on this machine, with 4 cores (task slots)
    .appName("labs")
    .getOrCreate())
spark.sparkContext.setLogLevel("WARN")
print(spark.version, spark.sparkContext.uiWebUrl)   # the Spark UI, while the session runs
  • local[4] runs the driverdriver: The process that runs your program, plans the work and sends tasks to executors. Learn more → and a single executorexecutor: A worker process on a cluster machine that runs tasks and holds data in memory. Learn more → inside one JVM on your machine, with 4 tasktask: The work for one partition in one stage, run on one CPU core. Learn more → slots. local[*] uses every core; local uses one, which hides parallelism, so avoid it for these labs.
  • Everything else is real Spark: the same planner, shufflesshuffle: Moving rows between machines so that all rows with the same key end up together. Needed by joins, groupBy and sorting, and usually the most expensive step of a job. Learn more →, AQEAQE: Adaptive Query Execution: Spark re-plans a running query using the real data sizes it has measured. Learn more → and Spark UI as on a cluster. Only the network and the number of machines are missing.
  • getOrCreate() returns the existing session if there is one. Settings that size the application (memory, cores, packages) only apply when the session is first created: restart Python to change them.

The Spark UI

While the session is running, open the address the setup prints (usually http://localhost:4040; 4041 and up if 4040 is taken). The labs refer to its tabs: Jobs and Stagesstage: A group of steps Spark can run without moving data between machines. A new stage starts at every shuffle. Learn more → for task times, shuffle and spillspill: Writing data to local disk because it does not fit in memory. Slower, but the job keeps running. Learn more →; Storage for cached data; SQL / DataFrame for the final plan of each query, with row counts per operator. The UI disappears when the session stops.

With Delta Lake

PySpark · Used by the Delta labs in the Lakehouse track
# pip install pyspark==4.2.0 delta-spark==4.4.1
from delta import configure_spark_with_delta_pip
from pyspark.sql import SparkSession

builder = (SparkSession.builder
    .master("local[4]")
    .appName("delta-labs")
    .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension")
    .config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog"))
spark = configure_spark_with_delta_pip(builder).getOrCreate()   # downloads the Delta jars on first run
spark.sparkContext.setLogLevel("WARN")
print(spark.version)

The delta-spark package adds the Python API (DeltaTable), and configure_spark_with_delta_pip adds the matching Delta jars, downloaded once into ~/.ivy2. The two configs enable Delta's SQL commands (MERGE, OPTIMIZE, VACUUM, ...) and let saveAsTable create Delta tables. To use them on a table by path, write delta.`/absolute/path`: a relative path there is not recognised, which is why the labs wrap paths in os.path.abspath. Delta and Spark versions must match: each Delta release lists the Spark versions it supports.

With Apache Iceberg

PySpark · Used by the Iceberg labs in the Lakehouse track
# pip install pyspark==4.1.3   (Iceberg 1.12 ships a runtime for Spark 4.1, not yet for 4.2)
from pyspark.sql import SparkSession

spark = (SparkSession.builder
    .master("local[4]")
    .appName("iceberg-labs")
    .config("spark.jars.packages", "org.apache.iceberg:iceberg-spark-runtime-4.1_2.13:1.12.0")
    .config("spark.sql.extensions", "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions")
    .config("spark.sql.catalog.local", "org.apache.iceberg.spark.SparkCatalog")   # a catalog named "local"
    .config("spark.sql.catalog.local.type", "hadoop")                             # metadata in plain files
    .config("spark.sql.catalog.local.warehouse", "lab-warehouse")                 # a folder next to your script
    .getOrCreate())
spark.sparkContext.setLogLevel("WARN")
print(spark.version)

Iceberg ships one runtime jar per Spark minor version, and spark.jars.packages downloads it on first start. The local catalog stores table metadata as files under lab-warehouse/, so tables are named local.db.table. Production setups point the catalog at a REST catalog, Glue, Hive or Nessie instead; the table commands stay the same. Use a separate virtual environment from the Delta one, since the two need different Spark versions here.

With Apache Hudi

PySpark · Used by the Hudi lab in the Lakehouse track
# pip install pyspark==4.1.3   (Hudi 1.2 ships a bundle for Spark 4.1)
from pyspark.sql import SparkSession

spark = (SparkSession.builder
    .master("local[4]")
    .appName("hudi-labs")
    .config("spark.jars.packages", "org.apache.hudi:hudi-spark4.1-bundle_2.13:1.2.1")
    .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")       # Hudi requires Kryo
    .config("spark.kryo.registrator", "org.apache.spark.HoodieSparkKryoRegistrar")
    .config("spark.sql.extensions", "org.apache.spark.sql.hudi.HoodieSparkSessionExtension")
    .config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.hudi.catalog.HoodieCatalog")
    .getOrCreate())
spark.sparkContext.setLogLevel("WARN")
print(spark.version)

Hudi needs Kryo serialization and its own catalog and SQL extensions. It also expects absolute table paths, so the Hudi lab wraps its path in os.path.abspath. It runs on the same Spark 4.1 environment as Iceberg.

With Spark Connect

PySpark · Used by the Spark Connect lab
# pip install "pyspark[connect]==4.2.0"
from pyspark.sql import SparkSession

# remote("local") starts a Spark Connect server on this machine and connects to it over gRPC.
spark = SparkSession.builder.remote("local[4]").appName("connect-labs").getOrCreate()
print(spark.version, type(spark).__module__)

Spark Connect separates your Python process from the Spark driver. remote("local[4]") starts a Connect server on your machine and connects to it, which is the quickest way to see what changes for client code. Start it in a fresh Python process: a session cannot switch between classic and Connect mode.

When it does not start

  • JAVA_GATEWAY_EXITED or "Java not found" — No Java, or an unsupported version. Install Java 17 and set JAVA_HOME to it.
  • Python worker version mismatch — Spark started workers with a different Python than your notebook. Set PYSPARK_PYTHON to your virtual environment's Python.
  • ClassNotFoundException for Delta or Iceberg classes — The package did not load: a typo in the package name, no internet on first start, or a Spark and Delta/Iceberg version mismatch.
  • Port 4040 in use — Another session is running. Spark picks 4041 and up; stop old sessions with spark.stop().
  • LOCATION_ALREADY_EXISTS on saveAsTable — The local catalog forgets tables when Python exits, but the files in spark-warehouse/ stay. Delete that folder, or the table's subfolder, and run again.

Common mistakes

  • Using local (one core) for performance experiments — With a single task slot, nothing runs in parallel and partitionpartition: A chunk of a DataFrame's rows. Spark processes each partition as one task, so partitions decide how much work runs in parallel. Learn more → effects disappear. Use local[4] or local[*].
  • Changing spark.driver.memory with spark.conf.set — Memory and cores are fixed when the JVM starts. Set them in the builder and restart Python.
  • Mixing Spark and Delta or Iceberg versions — Each Delta release and Iceberg runtime supports specific Spark versions; mismatches fail at startup or on first use.

Interview prep

THE QUESTION

"What is local mode, and how is it different from running on a cluster?"

In local mode the driver and one executor run in a single JVM on one machine, with as many task slots as given in local[N]. The engine is the same: planning, stages, shuffles, AQE, caching and the Spark UI all behave as on a cluster, so it is the right place to read plans and learn behaviour. What it cannot show is anything about the network and multiple executors: shuffle traffic between machines, executor loss and fetch failures, dynamic allocation, data locality, and real memory pressure across nodes.

Avoid saying: "local mode is just for testing, it is not real Spark". It runs the same engine; it only lacks multiple machines and the network.

What the interviewer asks next. Answer out loud first, then open the strong answer.

Follow-up"Which problems can you reproduce in local mode, and which can you not?"
Reproducible: plan choices (join strategies, pushdown, AQE decisions), skew within a stage, small-file and partition-count effects, caching behaviour, and most correctness bugs. Not reproducible: network-bound shuffle cost, executor loss and fetch failures, dynamic allocation, data locality, and container memory limits on a cluster manager.
Scenario"A job is slow in production. How would you use a laptop to investigate it?"
Take a representative sample of the input (or the same schema with generated data), run the job locally, and read the plan and the Spark UI: count Exchange nodes, check pushed filters and the join strategy, and look at per-task time and spill. Fix and compare plans locally, then confirm on the cluster with real volumes, since sizes change AQE and join decisions.
Trap"Setting spark.executor.memory in a local session gives the executor more memory."
In local mode the executor lives inside the driver JVM, so spark.driver.memory is what counts, and only when set before the JVM starts. spark.executor.memory has no effect locally.

What you learned

  • What you need installed, and how to check it
  • How to start a local Spark session, and what local[4] means
  • How to add Delta Lake, Iceberg or Hudi to a session
  • How to open the Spark UI and fix the common setup errors

Key takeaways

  • pip install pyspark gives you a complete Spark; Java 17 is the only other requirement.
  • local[4] runs driver and executor in one JVM with 4 task slots.
  • Delta and Iceberg are added with packages and two or three configs.
  • The Spark UI at localhost:4040 shows jobs, stages, storage and final plans.

Check yourself

3 questions

What does master("local[4]") mean?

Show the answer

One JVM on this machine with 4 task slots. Local mode runs everything in one JVM; the number is the count of concurrent tasks.

Why does spark.conf.set("spark.driver.memory", "4g") not change memory in a running session?

Show the answer

Memory is fixed when the JVM starts; set it in the builder before the session exists. Static settings are read once, at startup.

Where is the Spark UI while a local session runs?

Show the answer

http://localhost:4040 (or the next free port). Every SparkContext starts the UI, including local mode.

Keep going

Up next · lesson 2 of 31 · 7 min read
Driver, executors and the cluster manager
Who plans the work, who does it, and who hands out the machines in a Spark application.

Related lessons

Primary sources: PySpark installation · Delta Lake quickstart · Iceberg Spark quickstart