CertSafari
    Databricks Certified Associate Developer for Apache Spark· Lessons

    Domain 7 · Lesson 32/32

    Pandas UDF variants: iterator and Series to scalar

    Create and invoke Pandas UDF.

    10 min read
    3.12% of exam
    2 sources
    Published 3 Oct 2026
    Docs as of 30 Sep 2026

    What you will be able to do

    • Write an Iterator of Series UDF that initializes expensive state once and reuses it across batches
    • Write a UDF that takes several columns through an iterator of tuples of Series
    • Use a Series to scalar pandas UDF as an aggregate in groupBy().agg() and over a Window
    • Choose the right pandas UDF variant from its type hint and the API you will call it in

    1.Iterator of Series to Iterator of Series

    A Series to Series UDF does all its work inside one call per batch. If that work needs expensive setup, such as loading a machine learning model, the setup runs again for every batch. The iterator variant avoids this. Spark calls the function once and gives it an iterator of batches. The function yields one output batch for each input batch, so you can do the setup once before the loop. Spark still picks the variant from the type hint, here Iterator[pd.Series] -> Iterator[pd.Series].

    The simplest iterator UDF: yield one output Series per input batchpython
    @pandas_udf("long")
    def plus_one(iterator: Iterator[pd.Series]) -> Iterator[pd.Series]:
        for s in iterator:
            yield s + 1

    The length rule is relaxed here, but it still applies. Each yielded batch doesn't have to match the batch it came from, but the total output across all batches must be as long as the total input. To make sure resources acquired before the loop are released, the docs recommend a try/finally block or a context manager:

    Setting up state before the loop and releasing it in finallypython
    @pandas_udf("long")
    def plus_y(batch_iter: Iterator[pd.Series]) -> Iterator[pd.Series]:
        try:
            for x in batch_iter:
                yield x + y
        finally:
            pass  # release resources here, if any

    Checkpoint 1 of 5· Check yourself

    An Iterator of Series UDF receives 1,000 input rows split across several batches. Which rule applies to the rows it yields?

    Sources1

    2.Iterator of multiple Series: several input columns

    To pass several columns to an iterator UDF, declare the input as an iterator of tuples: Iterator[Tuple[pd.Series, ...]] -> Iterator[pd.Series]. Each element of the iterator is a tuple with one Series per column, all covering the same batch, and you unpack the tuple in the for loop. Everything else is the same as the single-column iterator: you can set up state once, and the total output length must match the total input length.

    Two columns arrive together as a tuple for each batchpython
    @pandas_udf("long")
    def multiply_two_cols(
            iterator: Iterator[Tuple[pd.Series, pd.Series]]) -> Iterator[pd.Series]:
        for a, b in iterator:
            yield a * b
    
    df.select(multiply_two_cols("x", "x")).show()

    Checkpoint 2 of 5· Check yourself

    You need to load a model once per task and score it against two columns, weight and height. Which signature fits?

    Sources1

    3.Series to scalar: pandas UDFs as aggregates

    Every variant so far returns as many rows as it receives. A Series to scalar UDF returns one value, so it works like a Spark aggregate function. Its type hint is pd.Series, ... -> Any, and in practice you replace Any with a specific scalar type such as float. The returnType must be a primitive data type, and the function can return a Python primitive like int or float or a NumPy scalar like numpy.float64. You can use it in select (aggregating the whole DataFrame), in groupBy().agg(), and over a Window.

    A mean aggregate used per grouppython
    @pandas_udf("double")
    def mean_udf(v: pd.Series) -> float:
        return v.mean()
    
    df = spark.createDataFrame(
        [(1, 1.0), (1, 2.0), (2, 3.0), (2, 5.0), (2, 10.0)], ("id", "v"))
    df.groupby("id").agg(mean_udf(df['v'])).show()
    The same UDF evaluated over a sliding windowpython
    w = Window.partitionBy('id').orderBy('v').rowsBetween(-1, 0)
    df.withColumn('mean_v', mean_udf("v").over(w)).show()

    There is a cost. Built-in aggregates can partially aggregate data before the shuffle, but a Series to scalar pandas UDF can't. All of a group's data is loaded into memory before the function runs, so one very large group can exhaust a worker's memory. Newer Spark releases add an Iterator of Series to scalar form, Iterator[pd.Series] -> float, which Databricks describes as memory-efficient grouped aggregation. It receives a group's data batch by batch and keeps running totals instead of holding the whole group in memory.

    Iterator of Series to scalar: accumulate across batches, return one valuepython
    @pandas_udf("double")
    def pandas_mean_iter(it: Iterator[pd.Series]) -> float:
        sum_val = 0.0
        cnt = 0
        for v in it:
            sum_val += v.sum()
            cnt += len(v)
        return sum_val / cnt

    Checkpoint 3 of 5· Check yourself

    A Series to scalar pandas UDF runs in groupBy("customer").agg(...) and fails with out-of-memory errors on one very large customer. What explains it?

    Checkpoint 4 of 5· Exam question

    An analyst wants to compute a weighted average price per `store_id` using a custom pandas UDF and Spark's grouped aggregation, so the UDF should take a Series of values from a group and return a single number for that group. Which definition and call correctly implements this?

    Sources12

    4.Choosing the variant from the type hint

    All of these variants are created the same way, with pandas_udf and a returnType. They differ only in their Python type hints, and Spark uses those hints to decide how to call the function. The table lists the four variants in the Databricks pandas UDF guide by signature and by where you invoke them.

    Pandas UDF variants by type hint and where you invoke them
    VariantType hintInput columnsOutput and typical APIs
    Series to Seriespandas.Series, ... -> pandas.SeriesOne or moreSame-length Series; select, withColumn
    Iterator of Series to Iterator of SeriesIterator[pandas.Series] -> Iterator[pandas.Series]Exactly oneSame total length; one-time state such as a loaded model
    Iterator of multiple Series to Iterator of SeriesIterator[Tuple[pandas.Series, ...]] -> Iterator[pandas.Series]MultipleSame total length; one-time state across several columns
    Series to scalarpandas.Series, ... -> AnyOne or moreOne primitive value; select, withColumn, groupBy.agg, pyspark.sql.Window

    Checkpoint 5 of 5· Match them up

    Match each type hint to the variant Spark infers from it

    Tap a term, then the definition that fits it.

    Sources1

    Exam traps

    Each one states something that sounds right. Open it to see what is actually true.

    1. 1.An Iterator[pd.Series] -> Iterator[pd.Series] UDF can take several columns, one per element of the iterator.Why is that wrong?

      That signature takes a single Spark column. For several columns, the hint must be an iterator of tuples of Series.

      Covered in Iterator of multiple Series: several input columns

    2. 2.A Series to scalar pandas UDF partially aggregates like built-in aggregates, so skewed groups are safe.Why is that wrong?

      It doesn't support partial aggregation. All of a group's data is loaded into memory before the function runs.

      Covered in Series to scalar: pandas UDFs as aggregates

    Practise it for real

    Create a pandas UDF by explicit wrapping, test it locally, invoke it on a Spark DataFrame, then compare it with a decorator-based aggregate.

    1. 1.Define multiply_func(a: pd.Series, b: pd.Series) -> pd.Series that returns a * b, and wrap it with multiply = pandas_udf(multiply_func, returnType=LongType()).

      Why: Explicit wrapping keeps the plain function available for local testing and gives you a separate UDF object for Spark.

      You should see: multiply is a pandas UDF, and multiply_func is still an ordinary Python function.

    2. 2.Call multiply_func(x, x) on x = pd.Series([1, 2, 3]).

      Why: The function must work on local pandas data, so you can check its logic without Spark.

      You should see: A pandas Series containing 1, 4, 9 with dtype int64.

    3. 3.Create df = spark.createDataFrame(pd.DataFrame(x, columns=["x"])) and run df.select(multiply(col("x"), col("x"))).show().

      Why: This calls the vectorized UDF on Spark, which runs the function once per Arrow batch.

      You should see: A column named multiply_func(x, x) with values 1, 4, 9.

    4. 4.Define @pandas_udf("double") def mean_udf(v: pd.Series) -> float: return v.mean(), then run it with df.groupby("id").agg(mean_udf(df['v'])) on the five-row (id, v) DataFrame from the Series to scalar section.

      Why: The float return hint turns the same pandas_udf call into an aggregate.

      You should see: One row per id: 1.5 for id 1 and 6.0 for id 2.

    5. 5.Register a Series to Series UDF with spark.udf.register("add_one", add_one) and run spark.sql("SELECT add_one(id) FROM range(3)").collect().

      Why: Registration lets you call the same pandas UDF from SQL.

      You should see: Rows with add_one(id) equal to 1, 2 and 3.

    Stuck? Get a nudge

    If the groupBy step returns one row instead of one per id, check that you called agg on df.groupby("id") rather than using df.select.

    Sources

    Every claim above is drawn from one of these pages, quoted as it was written on the date shown.

    1. 1.
      “useful when the UDF execution requires initializing some state, for example, loading a machine learning model file to apply inference to every input batch.”
      ↩︎ Iterator of Series to Iterator of Series
      “The length of the entire output in the iterator should be the same as the length of the entire input.”
      ↩︎ Iterator of Series to Iterator of Series
      “Wrap your code with try/finally or use context managers to ensure”
      ↩︎ Iterator of Series to Iterator of Series
      “The underlying Python function takes an iterator of a tuple of pandas Series.”
      ↩︎ Iterator of multiple Series: several input columns
      “You use a Series to scalar pandas UDF with APIs such as select, withColumn, groupBy.agg, and pyspark.sql.Window.”
      ↩︎ Series to scalar: pandas UDFs as aggregates
      “The return type should be a primitive data type”
      ↩︎ Series to scalar: pandas UDFs as aggregates
      “This article describes the different types of pandas UDFs and shows how to use pandas UDFs with type hints.”
      ↩︎ Choosing the variant from the type hint
      “The wrapped pandas UDF takes multiple Spark columns as an input.”
      ↩︎ Exam trap 1
      “This type of UDF does not support partial aggregation and all data for each group is loaded into memory.”
      ↩︎ Exam trap 2
      “The wrapped pandas UDF takes a single Spark column as an input.”
      ↩︎ Prediction
      “You specify the type hints as Iterator[Tuple[pandas.Series, ...]] -> Iterator[pandas.Series].”
      ↩︎ Checkpoint
      “This type of UDF does not support partial aggregation and all data for each group is loaded into memory.”
      ↩︎ Checkpoint
      “You express the type hint as pandas.Series, ... -> Any.”
      ↩︎ Checkpoint
    2. 2.
      “Example 6: Iterator of Series to Scalar - Memory-efficient grouped aggregation.”
      ↩︎ Series to scalar: pandas UDFs as aggregates

    Ready to test yourself?

    Practise the 11 questions on this subdomain.

    Spotted a mistake, or was something unclear? Tell us.