Skip to content

Snowpark Python, Java, and Scala

πŸ“– Snowpark Developer Guide - Snowpark overview

Overview

This document covers Snowpark development across Python, Java, and Scala, including the DataFrame API, UDF development, stored procedures, and integration patterns. For the Advanced Data Engineer exam, Python is the primary focus, but understanding cross-language capabilities is important.

Snowpark Python

Session Management

from snowflake.snowpark import Session
from snowflake.snowpark.functions import col, lit, when, sum, avg, count
from snowflake.snowpark.types import *

# Create session
session = Session.builder.configs({
    "account": "myaccount",
    "user": "myuser",
    "password": "mypassword",
    "role": "DATA_ENGINEER",
    "warehouse": "COMPUTE_WH",
    "database": "MY_DB",
    "schema": "PUBLIC"
}).create()

# Session context
session.use_warehouse("LARGE_WH")
session.use_database("ANALYTICS_DB")
session.use_schema("CURATED")

πŸ“– Snowpark Python Setup - Installation and configuration

DataFrame Operations

Reading Data

# From table
customers = session.table("customers")

# From SQL
active_orders = session.sql("SELECT * FROM orders WHERE status = 'ACTIVE'")

# From staged files
csv_df = session.read.option("field_delimiter", ",").option("skip_header", 1).csv("@my_stage/data.csv")
parquet_df = session.read.parquet("@my_stage/data.parquet")
json_df = session.read.json("@my_stage/data.json")

Transformations

# Filter
active = customers.filter(col("status") == "active")

# Select with expressions
selected = customers.select(
    col("id"),
    col("first_name"),
    col("last_name"),
    (col("first_name") + lit(" ") + col("last_name")).alias("full_name")
)

# Conditional columns
categorized = orders.with_column("size",
    when(col("amount") > 1000, lit("large"))
    .when(col("amount") > 100, lit("medium"))
    .otherwise(lit("small"))
)

# Aggregation
summary = orders.group_by("region", "product_category").agg(
    count("*").alias("order_count"),
    sum("amount").alias("total_revenue"),
    avg("amount").alias("avg_order_value")
)

# Join
enriched = orders.join(customers,
    orders["customer_id"] == customers["id"],
    join_type="left"
)

# Window functions
from snowflake.snowpark.functions import rank, row_number
from snowflake.snowpark import Window

window = Window.partition_by("region").order_by(col("revenue").desc())
ranked = sales.with_column("rank", rank().over(window))

πŸ“– DataFrame Operations - DataFrame reference

Writing Data

# Write to table (overwrite)
df.write.mode("overwrite").save_as_table("output_table")

# Write to table (append)
df.write.mode("append").save_as_table("output_table")

# Write to stage
df.write.csv("@my_stage/output/")
df.write.parquet("@my_stage/output/")

# Create or replace table
df.write.save_as_table("output_table", mode="overwrite", table_type="transient")

Python UDFs

Scalar UDF

from snowflake.snowpark.functions import udf
from snowflake.snowpark.types import StringType, IntegerType, FloatType

# Decorator-based registration
@udf(name="calculate_discount", return_type=FloatType(),
     input_types=[FloatType(), StringType()], replace=True,
     is_permanent=True, stage_location="@udf_stage")
def calculate_discount(price: float, tier: str) -> float:
    discounts = {"gold": 0.20, "silver": 0.10, "bronze": 0.05}
    return price * discounts.get(tier, 0)

# Use in DataFrame
orders.with_column("discount", calculate_discount(col("price"), col("tier")))

Vectorized UDF (Batch Processing)

from snowflake.snowpark.functions import pandas_udf
from snowflake.snowpark.types import PandasSeriesType, PandasDataFrameType
import pandas as pd

# Vectorized UDF processes pandas Series (much faster)
@pandas_udf(name="normalize", return_type=PandasSeriesType(FloatType()),
            input_types=[PandasSeriesType(FloatType())], replace=True)
def normalize(series: pd.Series) -> pd.Series:
    return (series - series.mean()) / series.std()
  • Vectorized UDFs process batches of rows as pandas Series
  • Significantly faster than row-at-a-time scalar UDFs for large datasets
  • Require pandas package declaration
  • Best for numerical/statistical operations

πŸ“– Vectorized UDFs - Batch processing

User-Defined Table Functions (UDTFs)

from snowflake.snowpark.types import StructType, StructField, StringType, IntegerType

# Class-based UDTF
class SplitAndCount:
    def __init__(self):
        self.count = 0

    def process(self, text: str, delimiter: str):
        for word in text.split(delimiter):
            self.count += 1
            yield (word.strip(), self.count)

    def end_partition(self):
        yield ("TOTAL", self.count)

# Register UDTF
split_udtf = session.udtf.register(
    SplitAndCount,
    output_schema=StructType([
        StructField("word", StringType()),
        StructField("position", IntegerType())
    ]),
    input_types=[StringType(), StringType()],
    name="split_and_count",
    replace=True
)

# Use with table function
result = df.join_table_function(split_udtf(col("text"), lit(",")))

πŸ“– UDTFs - Table function development

Stored Procedures

Basic Stored Procedure

from snowflake.snowpark import Session

@session.sproc(name="daily_etl", replace=True,
               packages=["snowflake-snowpark-python"])
def daily_etl(session: Session, target_date: str) -> str:
    # Read source data
    raw = session.table("raw_events").filter(col("event_date") == target_date)

    # Transform
    transformed = raw.select(
        col("event_id"),
        col("event_type"),
        col("user_id"),
        col("payload")
    ).filter(col("event_type").is_not_null())

    # Aggregate
    summary = transformed.group_by("event_type").agg(
        count("*").alias("event_count")
    )

    # Write
    summary.write.mode("overwrite").save_as_table(f"daily_summary_{target_date}")
    return f"Processed {transformed.count()} events for {target_date}"

Stored Procedure with Error Handling

@session.sproc(name="safe_etl", replace=True,
               packages=["snowflake-snowpark-python"])
def safe_etl(session: Session, table_name: str) -> str:
    try:
        source = session.table(table_name)
        row_count = source.count()
        if row_count == 0:
            return "No data to process"

        result = source.filter(col("is_valid") == True)
        result.write.mode("append").save_as_table("processed_data")

        # Log success
        session.sql(f"""
            INSERT INTO etl_log (table_name, status, rows_processed, timestamp)
            VALUES ('{table_name}', 'SUCCESS', {row_count}, CURRENT_TIMESTAMP())
        """).collect()

        return f"Processed {row_count} rows from {table_name}"
    except Exception as e:
        session.sql(f"""
            INSERT INTO etl_log (table_name, status, error_message, timestamp)
            VALUES ('{table_name}', 'FAILED', '{str(e)}', CURRENT_TIMESTAMP())
        """).collect()
        raise

πŸ“– Stored Procedures - Procedure guide

Snowpark Java and Scala

Java Example

import com.snowflake.snowpark_java.*;
import com.snowflake.snowpark_java.Functions.*;

// Session creation
Session session = Session.builder().configs(properties).create();

// DataFrame operations
DataFrame orders = session.table("orders");
DataFrame filtered = orders.filter(Functions.col("amount").gt(100));
DataFrame summary = filtered.groupBy(Functions.col("region"))
    .agg(Functions.sum(Functions.col("amount")).as("total"));

summary.write().mode(SaveMode.Overwrite).saveAsTable("regional_summary");

πŸ“– Snowpark Java - Java developer guide

Scala Example

import com.snowflake.snowpark._
import com.snowflake.snowpark.functions._

// Session creation
val session = Session.builder.configs(Map(...)).create

// DataFrame operations
val orders = session.table("orders")
val filtered = orders.filter(col("amount") > 100)
val summary = filtered.groupBy(col("region"))
  .agg(sum(col("amount")).as("total"))

summary.write.mode(SaveMode.Overwrite).saveAsTable("regional_summary")

πŸ“– Snowpark Scala - Scala developer guide

Language Comparison

Feature Python Java Scala
Package management Anaconda channel Maven Maven/SBT
Vectorized UDFs Yes (pandas) No No
ML libraries scikit-learn, xgboost Weka, DL4J Spark MLlib
Pandas support Native (to_pandas) No No
Community adoption Highest Moderate Lower
Performance Good (vectorized) Good (JVM) Good (JVM)

Package Management

Available Anaconda Packages

# Common packages available in Snowflake
# Data processing
import pandas as pd
import numpy as np

# ML/AI
from sklearn.ensemble import RandomForestClassifier
import xgboost as xgb
from scipy import stats

# Text processing
import re
import json

# Declare packages in UDF/procedure
@udf(packages=["pandas==2.0.3", "numpy", "scikit-learn==1.3.0"])
def my_function(...):
    ...

πŸ“– Third-Party Packages - Package availability

Custom Package Upload

# Upload custom code to stage
session.file.put("local_module.py", "@code_stage/libs/", auto_compress=False)

# Reference in UDF
@udf(imports=["@code_stage/libs/local_module.py"])
def my_function(x):
    import local_module
    return local_module.process(x)

Integration Patterns

Snowpark with Tasks

-- Call Snowpark procedure from a task
CREATE TASK daily_transform
  WAREHOUSE = transform_wh
  SCHEDULE = 'USING CRON 0 6 * * * UTC'
AS
  CALL daily_etl(CURRENT_DATE()::VARCHAR);

Snowpark with Streams

@session.sproc(name="process_stream", replace=True)
def process_stream(session: Session) -> str:
    stream_df = session.table("my_stream")
    new_rows = stream_df.filter(col("METADATA$ACTION") == "INSERT")
    if new_rows.count() > 0:
        new_rows.write.mode("append").save_as_table("target")
        return f"Processed {new_rows.count()} new rows"
    return "No new data"

Temporary vs Permanent Registration

Registration Scope Persistence Use Case
Temporary (default) Session only Dropped on session end Ad-hoc analysis
Permanent Database/schema Persists across sessions Production pipelines
# Permanent UDF (survives session end)
@udf(name="my_udf", is_permanent=True, stage_location="@udf_stage", replace=True)
def my_udf(...):
    ...

# Temporary UDF (session-scoped)
@udf(name="temp_udf")
def temp_udf(...):
    ...