A filter can make a Python function run again
As in Spark, a deterministic Python UDF can run more than once per row when the query filters on its output. Mark UDFs that are expensive or have side effects (model or API calls) with .asNondeterministic(), which makes them run once per row.
Check targets:
pysail==0.7.3 with pyspark-client==4.1.3The Python assertions below check this behavior. Details attributed to external documentation are outside those assertions. Newer releases may behave differently.
Run the check
Follow the test setup, then run from the repository root (sail-examples/). The harness applies the configuration declared in this check.
Sail
uv run pytest 'tests/test_facts.py::test_fact[udf-reruns-when-filtered[sail]]'Apache Spark (requires Java 17)
uv run pytest 'tests/test_facts.py::test_fact[udf-reruns-when-filtered[spark]]'Python code
udf-reruns-when-filtered.py40 lines
FACT = {
"id": "udf-reruns-when-filtered",
"title": "A filter can make a Python function run again",
"category": "python-udfs",
"text": """As in Spark, a deterministic Python UDF can run more than once per row when the query filters on its
output. Mark UDFs that are expensive or have side effects (model or API calls) with
`.asNondeterministic()`, which makes them run once per row.""",
"engines": ["sail", "spark"],
"order": 3,
}
import tempfile
from pathlib import Path
from pyspark.sql import functions as F
def calls_for(spark, nondeterministic: bool, rows: int) -> int:
log = Path(tempfile.mkdtemp()) / "calls.log"
log_path = str(log)
def body(x):
with open(log_path, "a") as f:
f.write("x\n")
return x
udf = F.udf(body, "long")
if nondeterministic:
udf = udf.asNondeterministic()
spark.range(rows).select(udf("id").alias("v")).filter("v >= 0").collect()
return len(log.read_text().splitlines())
def check(spark, engine):
rows = 40
deterministic = calls_for(spark, False, rows)
nondeterministic = calls_for(spark, True, rows)
assert deterministic > rows, f"expected re-runs for a deterministic UDF, got {deterministic}"
assert nondeterministic == rows, f"asNondeterministic() should run once per row, got {nondeterministic}"
return {"rows": rows, "deterministic_calls": deterministic, "nondeterministic_calls": nondeterministic}