mrkeyoor.com_
Sun 20 Sept 22:35 UTC
PyPIDataupdated 20 Sept 2026

pyspark review

PySpark 4.2.0 is the Python entry point to Apache Spark's distributed SQL, DataFrame, streaming, and machine-learning engine. Python code builds a logical plan, while Spark's JVM driver divides it into stages and sends tasks to local threads or cluster executors. The PyPI source package contains Spark's JARs, so it is much larger than an ordinary Python client. Version 4.2 adds PySpark and Spark Connect access to change-data-capture reads, turns Arrow exchange and Arrow-backed regular Python UDFs on by default, adds native geospatial SQL types, and drops official PyPy support.

Verdict

PySpark 4.2.0 used 473 MB and took 18.9 seconds to install in our sandbox before a JVM or SparkSession even started. Install it when you already need Spark's distributed engine; keep one-machine analytics on DuckDB or Polars unless the workload proves otherwise.

We installed it

Lab card: what happened when we installed pysparkScreenshot of pyspark documentation
Install✓ · 18.9s2 packages on disk · 473 MB
Importimport pyspark in 0.80s · pure Python · py.typed · requires Python >=3.10
Known vulns0(pip-audit)

Answers from our run

Does pyspark install cleanly?

Yes. In a fresh container with an empty cache, pip install pyspark finished in 19 seconds, leaving 2 packages and 473 MB on disk. pip-audit reported no known vulnerabilities.

What does pyspark need to run?

Python >=3.10, and nothing compiled: it is pure Python. In our run import pyspark succeeded in 0.80s, and the package ships py.typed for type checkers.

pyspark or polars: which should you use?

polars: Use it for multithreaded DataFrame work when the data and intermediate state fit on one machine. PySpark 4.2.0 used 473 MB and took 18.9 seconds to install in our sandbox before a JVM or SparkSession even started.

When should you not use pyspark?

The dataset and intermediate results fit on one server; DuckDB or Polars avoids a 473 MB package, JVM startup, stages, and shuffle configuration

API stability4/5SparkSession, DataFrame transformations, SQL functions, readers, writers, and Structured Streaming keep their established 4.x shapes in PySpark 4.2.0. Execution defaults changed underneath those calls: JVM-Python data exchange, regular Python UDFs, and UDTFs now use Arrow by default. Nullable integers passed to pandas UDFs also arrive as pandas extension dtypes. Existing code can still run while producing different types or raising errors at a different boundary, so upgrades need data-level tests.
Docs4/5The Apache site has versioned Python API pages, quick starts, installation matrices, a dedicated 4.1-to-4.2 upgrade section, SQL references, error classes, configuration tables, tuning guidance, and deployment manuals for each cluster manager. It documents the Java 17 floor, Arrow defaults, PyPy removal, and PyArrow 18.0.0 requirement. A real failure can still span the Python API, query plan, Hadoop connector, cluster manager, and cloud platform, which makes diagnosis a multi-manual job.
Maintenance5/5PyPI published 4.2.0 on 2026-07-14, and GitHub shows the Apache Spark repository pushed on 2026-08-26 with 43,882 stars. GitHub's combined counter lists 488 open issues and pull requests, while most planned work is tracked in Apache Jira. The 4.2 release page credits more than 250 contributors and over 1,700 resolved Jira tickets, spanning SQL, storage connectors, streaming, Python, Connect, Kubernetes, and the execution engine.
Ecosystem5/5The measured week records 10,471,039 PySpark downloads. Spark runs through major managed and self-hosted platforms, reads Hadoop-compatible storage, and integrates with table formats and catalogs used by large data systems. Python clients can use classic Spark or Spark Connect against the same engine. That reach comes with compatibility work across Spark 4.2, Java, Python, PyArrow, Hadoop libraries, storage connectors, cluster images, and each platform's release schedule.

Use it if

  • A join, aggregation, stateful stream, or scan exceeds one machine and needs Spark executors
  • The organization already operates Spark through Kubernetes, YARN, Databricks, EMR, Dataproc, or a standalone cluster
  • Python, SQL, Scala, and Java workloads need to share Spark tables, connectors, scheduling, and query plans
  • Batch DataFrames and Structured Streaming should use one schema and execution engine
Skip it if

Setup reality

We installed PySpark 4.2.0 in a fresh Python 3.12 sandbox in 18.9 seconds. The environment ended with 2 packages occupying 473 MB, and import pyspark took 0.80 seconds. pip-audit reported 0 known vulnerabilities. The distribution is pure Python, requires Python 3.10 or newer, includes py.typed, and uses Apache-2.0 licensing. Its metadata records 24 direct dependency entries across the base package and optional feature groups.

A successful import does not start Spark. Classic local or cluster execution still needs Java 17 or newer on PATH or through JAVA_HOME, plus a compatible Py4J package. The sql extra brings pandas, NumPy, and PyArrow; the connect extra adds those plus gRPC packages. Spark 4.2 requires PyArrow 18.0.0 or newer for its Arrow features and no longer lists PyPy as supported.

Choose the master URL, executor resources, serializer, shuffle partitions, warehouse, and connector JARs before the first SparkContext starts. getOrCreate() may return an existing context whose startup settings are already fixed. local[*] uses one machine but still launches a JVM and Spark services. A remote 4.2 cluster also needs reachable driver and executor networks, storage credentials, matching connectors, and a Python environment available on every worker.

Transformations remain lazy in 4.2; count(), show(), collect(), writes, and streaming starts trigger work. collect() and toPandas() copy the selected result into driver memory, so executor capacity cannot rescue an unbounded call. Cache only data reused by several actions, then unpersist it. Arrow exchange and Arrow execution for regular Python UDFs now default to true. Upgrade tests should cover nullable integer dtypes, schema coercion, NumPy conversion, and UDF output before jobs move to production.

Patterns

Start Spark with local worker threads create-local-session

from pyspark.sql import SparkSession

spark = (
    SparkSession.builder
    .appName("daily-etl")
    .master("local[*]")
    .config("spark.sql.shuffle.partitions", "8")
    .getOrCreate()
)

getOrCreate() can reuse a running SparkContext. Set context-level options before the first session starts or the new builder value may not take effect.

Avoid schema inference on CSV input read-csv-with-schema

orders = (
    spark.read
    .option("header", True)
    .schema("order_id STRING, user_id STRING, ts TIMESTAMP, amount DOUBLE")
    .csv("s3a://raw/orders/")
)

inferSchema requires another pass and can choose different types for different batches. s3a:// also needs compatible Hadoop AWS JARs and credentials.

Keep calculations inside Spark expressions filter-and-derive-columns

from pyspark.sql import functions as F

recent = (
    orders
    .filter(F.col("ts") >= F.lit("2026-01-01"))
    .withColumn("net_amount", F.col("amount") * F.lit(0.9))
    .select("order_id", "user_id", "net_amount")
)

Built-in expressions remain visible to Catalyst. Use them for common string, date, array, map, and arithmetic work before reaching for a Python UDF.

Compute totals for each user aggregate-by-key

totals = (
    orders
    .groupBy("user_id")
    .agg(
        F.sum("amount").alias("total_amount"),
        F.count_distinct("order_id").alias("order_count"),
    )
)

groupBy() normally causes a shuffle. A few dominant user_id values can create slow, oversized tasks even when the total cluster has spare capacity.

Send a small lookup to every executor broadcast-small-lookup

enriched = orders.join(
    F.broadcast(countries),
    on="country_code",
    how="left",
)

The broadcast side must fit in each executor's memory. Passing the shared key name produces one country_code column in the joined result.

Keep the newest event for each user select-latest-row-per-group

from pyspark.sql.window import Window

by_user = Window.partitionBy("user_id").orderBy(
    F.col("ts").desc(),
    F.col("event_id").desc(),
)
latest = (
    events
    .withColumn("position", F.row_number().over(by_user))
    .filter(F.col("position") == 1)
    .drop("position")
)

partitionBy() keeps the window distributed by user_id. The second ordering field makes the winner deterministic when timestamps match.

Run SQL against a DataFrame query-temporary-view

orders.createOrReplaceTempView("orders")

result = spark.sql("""
    SELECT user_id, SUM(amount) AS total
    FROM orders
    GROUP BY user_id
    ORDER BY total DESC
    LIMIT 20
""")

A temporary view belongs to its SparkSession and stores no independent copy of the rows. The SQL remains lazy until an action runs.

Lay out Parquet by event date write-partitioned-parquet

(
    events
    .write
    .mode("append")
    .partitionBy("event_date")
    .parquet("s3a://curated/events/")
)

A high-cardinality partition column creates many directories and small files. Partition by a field that future filters use and whose value count stays controlled.

Release cached blocks after the last action cache-reused-dataframe

valid = events.filter(F.col("is_valid")).cache()
try:
    valid.groupBy("type").count().write.mode("overwrite").parquet("/tmp/by-type")
    valid.groupBy("event_date").count().write.mode("overwrite").parquet("/tmp/by-day")
finally:
    valid.unpersist()

cache() is lazy and fills only after an action. unpersist() gives executor storage back after both consumers finish.

Check exchanges and join choices inspect-physical-plan

enriched.explain(mode="formatted")

The formatted plan identifies scans, exchanges, and join operators. Review it after filters and hints are applied, using representative table statistics.

Declare the output type of a Python UDF define-python-udf

from pyspark.sql import functions as F
from pyspark.sql.types import StringType

@F.udf(returnType=StringType())
def email_domain(value):
    return value.rsplit("@", 1)[-1] if value else None

users = users.withColumn("domain", email_domain("email"))

Spark 4.2 enables Arrow execution for regular Python UDFs by default. Test nulls and coercion, and prefer built-in functions when they can express the same result.

Checkpoint a streaming aggregation start-structured-stream

query = (
    events.writeStream
    .format("parquet")
    .option("path", "s3a://curated/live-events/")
    .option("checkpointLocation", "s3a://checkpoints/live-events/")
    .outputMode("append")
    .start()
)
query.awaitTermination()

The checkpoint stores offsets and state required for recovery. Give each query a stable, exclusive checkpoint location on durable storage.

Alternatives

PackageRegistryPick it when
polarsPyPIUse it for multithreaded DataFrame work when the data and intermediate state fit on one machine.
duckdbPyPIUse it for analytical SQL over local files or object-store Parquet when one process is sufficient.
daskPyPIUse it for distributed Python arrays, DataFrames, and task graphs when Python-native scheduling matters more than Spark SQL compatibility.
rayPyPIUse it for distributed Python functions, actors, serving, or training where the workload is broader than tabular Spark jobs.

More data guides

numpy · fsspec · pandas · sqlalchemy · pyarrow · lxml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.