CertSafari
    Databricks Certified Data Engineer Professional· Lessons

    Domain 1 · Lesson 3/46

    Session-scoped Python and pandas UDFs in PySpark

    Develop User-Defined Functions (UDFs) using Pandas, Python, and SQL, including Unity Catalog functions for the given constraints.

    11 min read
    1.92% of exam
    4 sources
    Published 2 Oct 2026
    Docs as of 30 Sep 2026

    What you will be able to do

    • Decide when a UDF is the right tool and when built-in Spark functions are better
    • Register a scalar Python UDF with spark.udf.register, udf() or the @udf decorator, with an explicit return type
    • Write null-safe UDF calls that do not depend on SQL short-circuiting
    • Choose between Series to Series, iterator, and Series to scalar pandas UDFs

    Key concept

    UDF scope: session-scoped vs Unity Catalog-governed — A UDF is either defined in your notebook or SparkSession and lives only there, or it is registered as a Unity Catalog function that is governed and shared. The two kinds have different scope, different features and different compute support, so you need to know which kind a question is about before you can answer it.

    1.When a UDF is the right tool

    A user-defined function lets you reuse custom logic that Spark does not provide out of the box. Databricks is clear that this has a cost. Built-in Apache Spark functions are optimized for distributed processing and perform better at scale, so use a UDF only for logic that is hard to express with them. The recommended uses are ad hoc queries, manual data cleaning, exploratory analysis and small to medium-sized datasets. Typical jobs are encryption, decryption, hashing, JSON parsing and validation. For large datasets and for anything that runs regularly or continuously, such as ETL jobs and streaming, Databricks recommends native Spark methods instead.

    Every UDF type is defined by two things: how many rows go in and come out, and where the function lives. A UC-governed function is registered in Unity Catalog and shared under its permissions. A session-scoped function exists only in the notebook or SparkSession that defined it. This page covers the session-scoped Python and pandas UDFs you write in PySpark.

    UDF types on Databricks: shape and where each can live
    UDF typeInput → outputScope
    Scalar UDFOne row in, one value outUnity Catalog governed or session-scoped
    Batch scalar UDFBatches of rows, 1:1 input/output row parityUnity Catalog governed or session-scoped
    Non-scalar (pandas) UDFWhole columns, flexible ratios (1:N or many:many)Session-scoped batch pandas UDFs
    UDAFMany rows in, one aggregated resultSession-scoped only
    UDTFMultiple rows (and possibly columns) per input rowUnity Catalog governed or session-scoped

    Checkpoint 1 of 7· Check yourself

    A team wants every analyst in the workspace to call its custom aggregation as a governed Unity Catalog function. According to the UDF type overview, which kind of UDF can never be Unity Catalog governed?

    Sources1

    2.Scalar Python UDFs: three ways to declare them

    A scalar Python UDF runs one row at a time. You can declare it in three ways. Use spark.udf.register to make it callable by name from SQL, for example from a %sql cell against a temp view. Use udf(func, returnType) to wrap a function for the DataFrame API. Or put the @udf("long") decorator on the function. In every case the return type is optional, and if you leave it out you get StringType. That is easy to miss when a numeric function quietly returns strings.

    Registering a UDF for SQL use, with an explicit LongType return typepython
    from pyspark.sql.types import LongType
    def squared_typed(s):
      return s * s
    spark.udf.register("squaredWithPython", squared_typed, LongType())
    The same UDF declared with annotation syntax and used in a DataFrame selectpython
    from pyspark.sql.functions import udf
    
    @udf("long")
    def squared_udf(s):
      return s * s
    df = spark.table("test")
    display(df.select("id", squared_udf("id").alias("id_squared")))

    Checkpoint 2 of 7· Fill the gap

    Complete the udf() call so that the squared values come back as 64-bit integers instead of the default type.

    from pyspark.sql.functions import udf
    from pyspark.sql.types import LongType
    squared_udf = udf(squared,  ? )
    df = spark.table("test")
    display(df.select("id", squared_udf("id").alias("id_squared")))

    Checkpoint 3 of 7· Exam question

    A data engineering team calls a Python UDF that scores millions of rows using a machine learning model loaded from a serialized file. Loading the model takes several seconds, and the current scalar `pandas_udf` reloads the model on every batch, causing high per-partition overhead. Which pandas UDF design lets the model load exactly once per Python worker process while still returning one score for every input row?

    Sources2

    3.Null handling and evaluation order

    Spark SQL, including the DataFrame API, does not fix the order in which an operator's inputs are evaluated. Boolean AND and OR do not short-circuit from left to right, and the query optimizer can reorder WHERE and HAVING clauses. A null check written to the left of a UDF call therefore protects nothing. There are two safe patterns. Either make the UDF itself null-aware, or put the call inside an IF or CASE WHEN branch that only runs when the value is not null.

    Two safe patterns: a null-aware UDF, and a conditional IF around the unsafe UDFpython
    spark.udf.register("strlen_nullsafe", lambda s: len(s) if not s is None else -1, "int")
    spark.sql("select s from test1 where s is not null and strlen_nullsafe(s) > 1") # ok
    spark.sql("select s from test1 where if(s is not null, strlen(s), null) > 1")   # ok

    Checkpoint 4 of 7· Check yourself

    A UDF fails intermittently with a TypeError on NULL input, even though the query filters col IS NOT NULL before calling it. Which change is the recommended fix?

    Sources2

    4.pandas UDFs: vectorized batches over Arrow

    A pandas UDF, also called a vectorized UDF, removes the row-at-a-time overhead. Spark uses Apache Arrow to move data in batches, and your function works on those batches with pandas. You declare one with pandas_udf, as a decorator or a wrapper, and the Python type hints tell Spark which of four forms you are writing. The simplest form is Series to Series. Spark splits the data into batches, calls your function once per batch, and concatenates the results. The output Series must be the same length as the input.

    Series to Series pandas UDF that multiplies two columnspython
    # Declare the function and create the UDF
    def multiply_func(a: pd.Series, b: pd.Series) -> pd.Series:
        return a * b
    
    multiply = pandas_udf(multiply_func, returnType=LongType())

    The iterator forms take an iterator of batches and yield an iterator of batches, and the total output length must still match the input. They exist so you can do expensive setup, such as loading a model, once before the loop instead of once per batch. Use try/finally or a context manager to release resources at the end. Iterator of Series takes one column. Iterator of multiple Series takes a tuple of Series per batch, one for each input column. Series to scalar is the odd one out: it works like a Spark aggregate function and can be used with select, withColumn, groupBy.agg and Window. It does not support partial aggregation, so all the data for each group is loaded into memory.

    Iterator of Series to Iterator of Series pandas UDFpython
    @pandas_udf("long")
    def plus_one(batch_iter: Iterator[pd.Series]) -> Iterator[pd.Series]:
        for x in batch_iter:
            yield x + 1
    The four session-scoped pandas UDF forms, identified by their type hints
    FormType hintPick it when
    Series to Seriespandas.Series, ... -> pandas.SeriesVectorizing a scalar operation in select or withColumn
    Iterator of Series to Iterator of SeriesIterator[pandas.Series] -> Iterator[pandas.Series]One input column plus one-time state initialization
    Iterator of multiple Series to Iterator of SeriesIterator[Tuple[pandas.Series, ...]] -> Iterator[pandas.Series]Several input columns plus one-time state initialization
    Series to scalarpandas.Series, ... -> AnyAn aggregation with groupBy.agg or a Window

    Checkpoint 5 of 7· Match them up

    Match each requirement to the pandas UDF form it calls for.

    Tap a term, then the definition that fits it.

    Checkpoint 6 of 7· Check yourself

    A Series to scalar pandas UDF used in groupBy().agg() runs out of memory on a heavily skewed key. What explains this?

    Checkpoint 7 of 7· Exam question

    A team runs `sales_df.groupBy("region").applyInPandas(compute_stats, schema=result_schema)` to execute a custom Python function that computes a variable number of summary rows per region using pandas operations that are not expressible in Spark SQL. Which two statements correctly describe how `applyInPandas` behaves in this scenario?(Select 2)

    Sources3

    Exam traps

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

    1. 1.A Python UDF's return type is inferred from what the function returns.Why is that wrong?

      Nothing is inferred. If you don't pass a return type, the UDF returns StringType.

      Covered in Scalar Python UDFs: three ways to declare them

    2. 2.Putting s IS NOT NULL AND before a UDF call protects the UDF from NULLs.Why is that wrong?

      Spark SQL fixes no evaluation order, and AND does not short-circuit. Do the null check inside the UDF or in an IF/CASE WHEN branch.

      Covered in Null handling and evaluation order

    3. 3.A Series to scalar pandas UDF aggregates incrementally like a built-in sum.Why is that wrong?

      It does not partially aggregate. Each group's data is loaded into memory in full.

      Covered in pandas UDFs: vectorized batches over Arrow

    Sources

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

    1. 1.
      “Use Apache Spark methods for operations on large datasets and any workloads that run regularly or continuously, including ETL jobs and streaming operations.”
      ↩︎ When a UDF is the right tool
      “Use UDFs for logic that is difficult to express with built-in Apache Spark functions.”
      ↩︎ When a UDF is the right tool
      “UDAFs are session-scoped only.”
      ↩︎ Checkpoint
    2. 2.
      “You can optionally set the return type of your UDF. The default return type is StringType.”
      ↩︎ Scalar Python UDFs: three ways to declare them
      “Use IF or CASE WHEN expressions to do the null check and invoke the UDF in a conditional branch”
      ↩︎ Null handling and evaluation order
      “The default return type is StringType.”
      ↩︎ Exam trap 1
      “For example, logical AND and OR expressions do not have left-to-right”
      ↩︎ Exam trap 2
      “This WHERE clause does not guarantee the strlen UDF to be invoked after filtering out nulls.”
      ↩︎ Prediction
      “Make the UDF itself null-aware and do null checking inside the UDF itself”
      ↩︎ Checkpoint
    3. 3.
      “pandas UDFs allow vectorized operations that can increase performance up to 100x compared to row-at-a-time Python UDFs.”
      ↩︎ pandas UDFs: vectorized batches over Arrow
      “You should specify the Python type hint as Iterator[pandas.Series] -> Iterator[pandas.Series].”
      ↩︎ pandas UDFs: vectorized batches over Arrow
      “This type of UDF does not support partial aggregation”
      ↩︎ Exam trap 3
      “This pandas UDF is useful when the UDF execution requires initializing some state, for example, loading a machine learning model file”
      ↩︎ Checkpoint
      “This type of UDF does not support partial aggregation and all data for each group is loaded into memory.”
      ↩︎ Checkpoint

    Also cited

    Continue to page 2 of 2

    Unity Catalog functions: SQL, Python and Batch Python UDFs

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