PySpark Interview Questions for Data Engineers — From Basics to Production

Master PySpark the way it is tested in real data engineering interviews. This course covers everything from Spark architecture and lazy evaluation to joins, window functions, Delta Lake, MERGE operations, SCD patterns, streaming, and performance tuning. Every question is written the way a real interviewer asks it, with a clear thinking framework before any code, production-grade PySpark solutions, and concise answers to the follow-up questions that separate good candidates from great ones. Built for engineers with hands-on Spark experience who want to go deeper and walk into interviews fully prepared

pysparkdata engineerpyspark interview questionsscenario basedproblem solving

Reading Data and Basic Transformations

The Interviewer Says

Let us start with the fundamentals before going into complex transformations. I want to see how you set up a Spark environment, read data, and perform some basic operations on a DataFrame. I have a CSV file with employee data. Walk me through how you would read it, explore it, and then apply some basic transformations like filtering, adding a new column, and renaming a column.

Input Data — employees.csv

emp_id,name,dept,salary,joining_date
E01,Karan,Finance,90000,2022-01-15
E02,Asha,IT,75000,2021-06-20
E03,Ravi,Finance,60000,2023-03-10
E04,Meena,HR,80000,2020-09-05
E05,Sunil,IT,70000,2022-11-30
E06,Divya,HR,55000,2023-07-18

How to Handle This in the Interview

Before writing anything, say: Let me walk through this in a logical order, environment setup first, then reading, then exploration, then transformations. Each step builds on the previous one.

Step 1: Understand SparkSession

  • Say: SparkSession is the single entry point to all Spark functionality. In older versions of Spark you had SparkContext, SQLContext, and HiveContext as separate objects. From Spark 2.0 onwards SparkSession unifies all of them. The first thing in any PySpark script is always creating or getting the SparkSession.

Step 2: Read with the right options

  • When reading a CSV, always think about three options, whether the first row is a header, whether Spark should infer the schema automatically or use a defined schema, and how to handle corrupt records. Say: For production pipelines I always define the schema explicitly rather than using inferSchema, since inferSchema does a full extra scan of the file just to guess types, which is expensive and can guess wrong.

Step 3: Explore before transforming

  • Show, printSchema, describe, and count are the four basic exploration steps. Mention all of them and why each one is useful rather than jumping straight into transformations.

Step 4: Common transformation operations

  • Filter rows, add a derived column using withColumn, rename a column using withColumnRenamed, select specific columns, and drop a column. These five operations cover the majority of basic interview transformation questions.

Solution

from pyspark.sql import SparkSession from pyspark.sql import functions as F from pyspark.sql.types import ( StructType, StructField, StringType, IntegerType, DateType ) # Step 1: Create SparkSession spark = SparkSession.builder \ .appName("EmployeeTransformations") \ .getOrCreate() # Step 2: Define schema explicitly (preferred over inferSchema in production) schema = StructType([ StructField("emp_id", StringType(), nullable=True), StructField("name", StringType(), nullable=True), StructField("dept", StringType(), nullable=True), StructField("salary", IntegerType(), nullable=True), StructField("joining_date", DateType(), nullable=True) ]) # Step 3: Read CSV with defined schema df = spark.read \ .option("header", "true") \ .option("mode", "DROPMALFORMED") \ .schema(schema) \ .csv("employees.csv") # Step 4: Explore the DataFrame df.show() df.printSchema() df.describe().show() print("Total rows:", df.count()) print("Total partitions:", df.rdd.getNumPartitions()) # Step 5: Filter rows where salary is greater than 70000 df_filtered = df.filter(F.col("salary") > 70000) df_filtered.show() # Step 6: Add a new column for annual bonus as 10 percent of salary df_with_bonus = df_filtered.withColumn( "annual_bonus", F.round(F.col("salary") * 0.10, 2) ) df_with_bonus.show() # Step 7: Add a designation column based on salary df_with_designation = df_with_bonus.withColumn( "designation", F.when(F.col("salary") > 80000, "Senior") .when(F.col("salary") > 70000, "Mid Level") .otherwise("Junior") ) df_with_designation.show() # Step 8: Rename a column df_renamed = df_with_designation.withColumnRenamed("name", "employee_name") df_renamed.show() # Step 9: Select specific columns only df_final = df_renamed.select( "emp_id", "employee_name", "dept", "salary", "annual_bonus", "designation" ) df_final.show() # Step 10: Drop a column df_dropped = df_final.drop("annual_bonus") df_dropped.show()

What to say: I always define the schema explicitly in production rather than using inferSchema because inferSchema triggers a full additional scan of the source file just to guess data types, doubling the read cost before any actual processing begins. I also chain transformations rather than writing each one as a separate variable where possible, since PySpark transformations are lazy and the actual computation only happens when an action like show or count is called. Each withColumn, filter, and select call just adds to the logical plan without executing anything.

Expected Output — df_final

emp_idemployee_namedeptsalaryannual_bonusdesignation
E01KaranFinance900009000.0Senior
E04MeenaHR800008000.0Mid Level

Follow-up Questions & Answers

Q1. What is the difference between inferSchema and defining the schema manually? When would you use each?

inferSchema tells Spark to automatically detect the data type of each column by scanning the entire dataset first before reading it, which means Spark reads the file twice, once to infer types and once to actually load the data. This doubles the read cost and can guess wrong, for example reading a column of IDs like 001, 002 as integers and silently dropping the leading zeros, or reading a date column as a string because the format does not match what Spark expects. Defining the schema manually with StructType and StructField gives you full control over every column's type and nullability, costs no extra scan, and fails fast with a clear error if the data does not match rather than silently producing wrong types. In production always define the schema. inferSchema is acceptable only for quick exploratory work in a notebook where speed of prototyping matters more than correctness guarantees.

Q2. What is lazy evaluation in Spark and why does it matter?

Lazy evaluation means Spark does not actually execute any transformation, meaning filter, withColumn, select, join, and so on, the moment you call them. Instead it builds up a logical plan describing what you want to do. The actual computation only happens when you call an action like show, count, collect, write, or take. This matters for two reasons. First it allows Spark's Catalyst optimiser to look at the entire logical plan before executing it and rearrange or optimise the steps, for example pushing filter conditions earlier in the plan so less data flows through later stages. Second it means you can chain many transformations cheaply and the cost is only paid once when the action fires. The common misconception is thinking each transformation line executes immediately the way Python list operations do, which leads to confusion about when data is actually being processed.

Q3. What is the difference between show, collect, and take in PySpark?

All three trigger execution but they behave very differently in terms of where the data goes and how much of it moves.

# show prints up to N rows directly to the console, data stays in the cluster df.show(10) # collect brings ALL rows from every partition into the driver's memory as a Python list # dangerous on large datasets, can crash the driver with an out of memory error rows = df.collect() # take brings only the first N rows to the driver, much safer than collect first_5 = df.take(5) # head is an alias for take first_3 = df.head(3)

What to say: I use show for quick visual inspection in development and notebooks. I never use collect on a large DataFrame in production since it moves the entire dataset to the driver node which has limited memory, a DataFrame with a billion rows would almost certainly crash the driver. If I genuinely need data in Python I use take with a reasonable limit, or write it to a file and read it back in a controlled way.

Q4. What is the difference between withColumn and select for adding a new column?

Both can add columns to a DataFrame but they have different implications for performance and readability.

# withColumn adds or replaces one column while keeping all existing columns df_with_bonus = df.withColumn("annual_bonus", F.col("salary") * 0.10) # select explicitly lists every column you want in the output # useful when you want to reorder, rename, and add in one step df_with_bonus = df.select( "*", (F.col("salary") * 0.10).alias("annual_bonus") ) # For many columns, chaining multiple withColumn calls can be expensive # in older Spark versions because each call creates a new plan node # select with all columns at once is more efficient for wide transformations df_transformed = df.select( F.col("emp_id"), F.col("name"), F.col("salary"), (F.col("salary") * 0.10).alias("annual_bonus"), F.when(F.col("salary") > 80000, "Senior").otherwise("Junior").alias("designation") )

What to say: For adding one or two columns withColumn is clear and readable. For adding many columns at once or when you also want to reorder and rename in the same step, select with explicit column expressions is more efficient since it creates a single plan node rather than stacking multiple withColumn nodes. In Spark versions before 3.0, chaining many withColumn calls on a very wide DataFrame could cause performance issues due to plan explosion, which select avoids entirely.

Q5. What are the different read modes in Spark and when would you use each?

When reading structured files like CSV or JSON, Spark has three modes for handling malformed or corrupt records.

# PERMISSIVE (default): reads all rows, sets corrupt fields to NULL # and optionally stores the bad row in a _corrupt_record column df_permissive = spark.read \ .option("header", "true") \ .option("mode", "PERMISSIVE") \ .option("columnNameOfCorruptRecord", "_corrupt_record") \ .schema(schema) \ .csv("employees.csv") # DROPMALFORMED: silently drops any row that cannot be parsed df_drop = spark.read \ .option("header", "true") \ .option("mode", "DROPMALFORMED") \ .schema(schema) \ .csv("employees.csv") # FAILFAST: throws an exception immediately on the first bad row df_strict = spark.read \ .option("header", "true") \ .option("mode", "FAILFAST") \ .schema(schema) \ .csv("employees.csv")

What to say: PERMISSIVE is the default and the most forgiving, it keeps all rows and surfaces bad data in a separate column so you can investigate it later without losing the good rows. I use this in data ingestion pipelines where I want to capture and quarantine bad records rather than drop them silently. DROPMALFORMED is useful when bad rows are expected and genuinely useless, for example log files where a handful of corrupted lines should not stop the entire pipeline. FAILFAST is useful in strict validation scenarios where any bad record means the source data has a serious quality problem that must be fixed before processing can continue, common in financial or compliance pipelines where partial data would be worse than no data.