PySpark Improvements

Arrow-Native UDFs (@arrow_udf)

Spark 4.1.1 introduces the @arrow_udf decorator for scalar functions that accept and return pyarrow.Array objects directly — bypassing Pandas conversion overhead entirely.

import pyarrow as pa import pyarrow.compute as pc from pyspark.sql.functions import arrow_udf from pyspark.sql.types import DoubleType @arrow_udf(returnType=DoubleType()) def discount_price(prices: pa.Array, discount: pa.Array) -> pa.Array: return pc.subtract(prices, pc.multiply(prices, discount)) df = spark.createDataFrame([(100.0, 0.1), (200.0, 0.2)], ["price", "discount"]) df.withColumn("final_price", discount_price("price", "discount")).show()

Arrow-Native UDTFs (@arrow_udtf)

The @arrow_udtf decorator enables table functions that process entire pyarrow.RecordBatch objects at once, rather than row-by-row — dramatically faster for splitting and exploding operations.

import pyarrow as pa from pyspark.sql.functions import arrow_udtf from pyspark.sql.types import StructType, StructField, StringType @arrow_udtf(returnType=StructType([StructField("word", StringType())])) class WordSplitter: def eval(self, batch: pa.RecordBatch) -> pa.RecordBatch: words = [] for text in batch.column("text").to_pylist(): words.extend(text.split()) return pa.RecordBatch.from_pydict({"word": words}) spark.udtf.register("word_splitter", WordSplitter) spark.sql("SELECT word_splitter(text) FROM documents").show()

Python Worker Logging

Debugging Python UDFs has historically been difficult because logs get lost in executor stdout/stderr. Spark 4.1.1 introduces dedicated UDF log capture, exposable via a built-in table-valued function.

Enable:

spark.sql.pyspark.worker.logging.enabled=true

Example:

import logging from pyspark.sql.functions import udf from pyspark.sql.types import StringType logger = logging.getLogger("my_udf") @udf(returnType=StringType()) def classify(value): logger.info(f"Processing value: {value}") if value > 100: return "high" return "low" df = spark.createDataFrame([(50,), (150,)], ["val"]) df.withColumn("category", classify("val")).show() # Query captured UDF logs spark.sql("SELECT * FROM python_worker_logs()").show()

Python Data Source API with Filter Pushdown

The Python Data Source API (for custom data sources written in Python) gains filter pushdown in Spark 4.1.1 — the data source can now receive query predicates from the optimizer and apply them at the source, reducing data movement.

from pyspark.sql.datasource import DataSource, DataSourceReader class MyDataSource(DataSource): def reader(self, schema): return MyReader(schema, self.options) class MyReader(DataSourceReader): def pushFilters(self, filters): # receive filters like [GreaterThan("id", 100)] self.pushed_filters = filters return filters # return filters handled by the source def read(self, partition): # apply self.pushed_filters when reading data for row in self._fetch_data(self.pushed_filters): yield row spark.dataSource.register(MyDataSource, "my_source") # Filter id > 100 will be pushed to MyReader.pushFilters() df = spark.read.format("my_source").load().filter("id > 100") df.show()

Python UDTFs

Python UDTFs produce multiple rows per input row — ideal for exploding, splitting, or generating records from a single input.

from pyspark.sql import SparkSession spark = SparkSession.builder.appName("Python UDTF").getOrCreate() def split_udtf(s): for char in s: yield (char,) spark.udtf.register("split_udtf", split_udtf) spark.sql("SELECT split_udtf(column_name) FROM table_name").show()

Pandas 2.x Support

Spark 4.1.1 supports pandas 2.x with Arrow-backed conversions for fast, zero-copy exchange between pandas and Spark DataFrames.

import pandas as pd from pyspark.sql import SparkSession spark = SparkSession.builder \ .config("spark.sql.execution.arrow.pyspark.enabled", "true") \ .appName("Pandas 2.x") \ .getOrCreate() pdf = pd.DataFrame({"id": [1, 2, 3], "value": ["a", "b", "c"]}) sdf = spark.createDataFrame(pdf) result = sdf.withColumn("upper_value", sdf.value.upper()).toPandas() print(result)

PySpark UDF Unified Profiler

Profiles PySpark UDFs for CPU and memory usage to identify bottlenecks.

from pyspark.sql.functions import udf from pyspark.sql.types import IntegerType spark.conf.set("spark.python.profile", "true") spark.conf.set("spark.python.profile.memory", "true") @udf(returnType=IntegerType()) def square(x): return x * x data = [("Alice", 1), ("Bob", 2), ("Charlie", 3)] df = spark.createDataFrame(data, ["name", "age"]) df.withColumn("age_squared", square(df.age)).show() spark.show_profiles()


  Last updated