Know when to move from pure Python to pandas, Polars, or Spark, and how to narrate those tradeoffs in interviews.
⏱ 18 min readTopics chapter readerLevel · Advanced
01 · Overview
Start here
⏱ 6 min · Topic 1 of 19
This chapter works one Python interview problem from the first question to a bigger file. You use what you learned before: dicts, generators, Counter, sorting, bad rows, type hints and tests.
The problem: you get a CSV file of orders. Return the top N customers by paid revenue.
Paid revenue is the sum of amount over the orders with status paid. The file is the shop again:
The shop ordersworked example
Python
Input data
orders6 rows
order_id
customer_id
status
amount
order_date
101
1
paid
40.00
2026-01-03
102
1
paid
25.00
2026-01-10
103
2
refunded
60.00
2026-01-11
104
3
paid
15.50
2026-01-15
105
3
pending
NULL
2026-01-20
106
4
paid
80.00
2026-01-21
# orders.csv has these six rows below a header line.# Every value in a CSV file is text.
Order 105 has no amount. In a CSV file that is an empty text.
You work in six steps:
Ask questions and say the problem back.
Write examples and edge cases, with the answer by hand.
Write a simple first version.
Test it on the shop rows.
Say the cost in plain words.
Make it work for a big file.
The last sections give you a script to say aloud and a plan for a 45-minute round.
02 · Step 1
Ask questions first
⏱ 4 min · Topic 2 of 19
Do not start with code. First say the problem back in your own words. For example: "You want the top N customers, by the sum of their paid amounts."
Then ask short questions. Each answer removes a guess. These are the questions, with the answers this chapter assumes:
Question
Answer we assume
How big is the file?
Too big for memory. Read it once.
Which orders count?
Only status paid. Refunded and pending do not.
What if amount is empty?
Fine on a pending order. On a paid order it is a bad row.
What is a bad row?
No customer_id, or an amount that is not a number or is below 0.
What do we do with bad rows?
Skip them and count them. Stop if more than 20% are bad.
What if two customers tie?
The smaller customer_id comes first.
What if N is bigger than the customer count?
Return all customers.
Interview tip
Interviewers can only judge your thinking if you say it. Ask first, then write.
03 · Step 2
Examples and edge cases
⏱ 5 min · Topic 3 of 19
Work the shop rows by hand before you write code. The result is your first test. This table decides, for each order, if it counts:
order_id
customer_id
counts?
why
101
1
yes, +40.00
status is paid
102
1
yes, +25.00
status is paid
103
2
no
refunded, even though it has an amount
104
3
yes, +15.50
status is paid
105
3
no
pending, and the amount is empty
106
4
yes, +80.00
status is paid
The totals are: customer 4 has 80.00, customer 1 has 65.00 (40.00 plus 25.00) and customer 3 has 15.50. Customer 2 has no paid order. So the top 3 are customers 4, 1 and 3.
Now list the edge cases. An edge case is an input at the border of what you expected:
An empty amount. Skip it on a pending order. Reject it on a paid order.
A refunded order. It has an amount (60.00), but it does not count.
An unknown customer, such as customer 9. We count by customer_id, so we do not need the customers table.
A customer with no orders, such as Omar (customer 5). He does not appear.
An empty file. The answer is an empty list.
04 · Step 3
A simple first version
⏱ 5 min · Topic 4 of 19
Write the simplest code that is correct. The idea has three parts: add up the paid amounts for each customer, sort the totals, and take the first N.
rows is a list of dicts, one dict per order. The type hints say what goes in and what comes out.
Read it in four steps:
Counter gives 0 to a customer it has not seen, so += works with no check.
The status test skips refunded and pending orders.
The key (-item[1], int(item[0])) sorts by total, biggest first. Equal totals sort by customer_id as a number.
[:n] keeps the first N items.
Watch out
A common shortcut is key=lambda item: (item[1], item[0]) with reverse=True. It puts the biggest total first, but it also flips the tie-break: on equal totals, the larger customer_id comes first.
05 · Step 4
Test it on the shop rows
⏱ 5 min · Topic 5 of 19
Run the function on the six shop rows. The text below is the CSV file. csv.DictReader turns each line into a dict:
Run it, and compare with your hand answerworked example
Run these with pytest. Each assert stops the test with an error if the two sides differ.
06 · Step 5
Say the cost in words
⏱ 6 min · Topic 6 of 19
Interviewers ask: how fast is it, and how much memory does it use? Let R be the number of rows and C the number of customers.
Part
Cost
In plain words
Add up the totals
time grows with R
The loop touches each row once.
Sort the totals
time grows a little faster than C
Sorting C items costs about C times log C.
Memory for totals
grows with C
One number per customer, not one per row.
Memory for the list of rows
grows with R
Every row stays in memory. Step 6c removes this.
Big-O is a short way to write this. Time is O(R + C log C).
The totals need O(C) memory, but the list of rows still needs O(R). Step 6c removes that list.
log C grows very slowly: for a million customers it is only about 20.
Say it in words too: "Time grows with the rows. Memory grows with the customers."
One small improvement: if C is huge and N is small, heapq.nlargest(n, ...) finds the top N without sorting every total. Mention it. Do not code it first.
Its key must still put the smaller id first on ties.
07 · Step 6a
Money needs Decimal
⏱ 5 min · Topic 7 of 19
A float stores many decimal numbers a little wrong. Decimal stores them exactly.
You met Decimal in the chapter Types, Time, Nulls & Numeric Correctness. Compare the two:
Now the amounts keep two decimals: 80.00, 65.00 and 15.50. This is the answer the problem asks for.
08 · Step 6b
Bad rows and the limit
⏱ 6 min · Topic 8 of 19
A real file has bad rows. With the Decimal version, one paid order with an empty amount stops the run: Decimal("") raises InvalidOperation. You need a check, a count and a limit.
The check raises ValueError for one bad row, as in the chapter Validation, Contracts & Error Handling. The loop counts the bad row and goes on. At the end, the limit decides if the whole run is bad:
Check, count and limitworked example
Python
fromcollectionsimportCounterfromcollections.abcimportIterablefromdecimalimportDecimal,InvalidOperationdefcheck(row:dict[str,str])->tuple[str,Decimal]:ifnotrow["customer_id"]:raiseValueError("customer_id is missing")try:amount=Decimal(row["amount"])exceptInvalidOperation:raiseValueError("amount is not a number")ifnotamount.is_finite()oramount<0:raiseValueError("amount must be 0 or more")returnrow["customer_id"],amountdeftop_customers(rows:Iterable[tuple[int,dict[str,str]]],n:int,max_reject:float=0.20)->tuple[list[tuple[str,Decimal]],dict[str,object]]:totals:Counter[str]=Counter()reasons:Counter[str]=Counter()first_bad:list[tuple[int,str]]=[]seen=0forline,rowinrows:seen+=1ifrow["status"].strip().lower()!="paid":continuetry:customer_id,amount=check(row)exceptValueErroraserror:reasons[str(error)]+=1iflen(first_bad)<3:first_bad.append((line,str(error)))continuetotals[customer_id]+=amountrejected=sum(reasons.values())ifseenandrejected/seen>max_reject:raiseValueError(f"too many bad rows: {rejected} of {seen}")ranked=sorted(totals.items(),key=lambdaitem:(-item[1],int(item[0])))report={"seen":seen,"rejected":rejected,"first_bad":first_bad}returnranked[:n],report
rows is now a sequence of (line number, row) pairs, so a rejected row can say where it is. Only paid rows are checked, because only they add to a total.
The status test now ignores spaces and capital letters, so " Paid" also counts.
Now add four rows to the shop file. Rows 107 and 108 are the first run. Rows 109 and 110 make the second run worse:
{'seen': 8, 'rejected': 1, 'first_bad': [(9, 'customer_id is missing')]}
too many bad rows: 3 of 10
start=2 because line 1 of the file is the header. First run: order 107 is for customer 9, who is unknown. It counts 12.00, and the top 3 do not change. Order 108 has no customer_id, so it is rejected: 1 of 8 is 12.5%, under the limit. Second run: 108, 109 and 110 are bad, 3 of 10 is 30%, so the run stops.
Watch out
Say which rows you divide by. Here the limit counts all rows seen, not only paid rows. A different choice gives a different percentage.
09 · Step 6c
Stream the big file
⏱ 8 min · Topic 9 of 19
So far the rows sat in a list. A big file does not fit in memory. A generator reads one row at a time and forgets it.
The function above already accepts a stream, because its loop reads one pair at a time.
This code writes a test file of 100,000 orders. Then it streams the file through top_customers:
(i % 50) + 1 is 50 only when the customer id is a multiple of 50. Customers 50, 100, 150 and so on up to 1000 each have 100 paid orders of 50.00. That is 5000.00 for 20 customers, so they tie. int(item[0]) sorts the ties by id as a number: 50, 100, 150. With the id as text, "100" would come before "50".
Now see the memory. tracemalloc measures the most memory Python used. Compare the stream with a list of all the rows. Your numbers will be close, not the same:
A generator can be read only once. A second loop over the same generator sees no rows. Call read_orders again for each pass.
10 · Step 6d
What grows with the input?
⏱ 5 min · Topic 10 of 19
Streaming is not enough. Ask of every variable: what makes it grow? This table does it for our code:
Variable
Grows with
Bound
the file reader
nothing
one row at a time
totals
the number of customers
one entry per customer
reasons
the kinds of problem
three kinds
first_bad
nothing
at most 3 items
Look at first_bad. A list of every bad row would grow with the bad rows.
A file with millions of bad rows would fill memory. So the code keeps three examples and a count.
Watch out
Saying "I stream" is not enough if you keep a dict of every customer. If the customers are too many for memory, the answer is a database or Spark, not a faster loop.
11 · Speed
Measure before you change
⏱ 5 min · Topic 11 of 19
Do not guess where the time goes. Time each part, then change only the slowest part.
This code times reading alone, and then reading plus adding up:
Time two parts with perf_counterworked example
Python
fromtimeimportperf_counterstart=perf_counter()forline,rowinread_orders("big_orders.csv"):passread_seconds=perf_counter()-startstart=perf_counter()top_customers(read_orders("big_orders.csv"),3)total_seconds=perf_counter()-startprint(f"read only: {read_seconds:.2f} s")print(f"read and add up: {total_seconds:.2f} s")
The times depend on your computer, so no output is shown. The gap between the two lines is the cost of the check and the sums.
If reading alone is most of the time, faster Python code will not help much. The file is the slow part. If the gap is large, look at check and the sums.
Two standard tools help more. timeit runs a small piece of code many times to compare versions.
cProfile shows which function takes the most time in a full run.
12 · Speed
Name the slow part
⏱ 5 min · Topic 12 of 19
A slow program is slow in one place: the processor, the memory, the disk or the network. Each one has a different first move:
What you see
Slow part
First move
One processor core at 100%
CPU
do less work per row, or use processes
Memory grows with the rows
Memory
stream, and stop keeping rows
The program waits for disk or database
I/O
read and write in bigger batches
The program waits for a web service
Network
ask for pages, retry, limit parallel calls
Python has a lock called the GIL (global interpreter lock). It lets only one thread run Python code at a time.
So threads do not speed up a CPU-heavy loop. The chapter Concurrency, Parallelism & Performance explains more.
A process is a separate running copy of Python. I/O means reading and writing (input and output).
Interview question
Your job is slow. What do you do? Say: I measure first, and I name the slow part as CPU, memory, I/O or network. Then I change only that part.
13 · Scale
Choose the right tool
⏱ 5 min · Topic 13 of 19
Pure Python is not always the answer. For our problem, the same result in SQL is short. This query runs on the shop orders:
Same three customers, same totals. SQL skips order 105 on its own: its status is pending.
Here is when each tool fits:
Tool
Use it when
Watch out
Pure Python stream
one pass, custom checks, small state
you write the state yourself
pandas
the data fits in memory and you work by column
memory fills up fast
SQL
the data already lives in a database or warehouse
a big query can cost money
Spark
the data or the state is too big for one machine
a cluster is more to run and debug
A quick reminder from the Spark chapters: Spark splits a big table into partitions and works on them on many machines. A shuffle moves rows between machines, and it is the costly step.
Interview question
When would you move to Spark? Say: when the data or the state no longer fits one machine, or the data already lives on a cluster. I would first try a stream and measure it.
14 · Interview
Say it aloud
⏱ 4 min · Topic 14 of 19
Speak while you work. This script is a model. Use your own words, and keep each sentence short:
"Let me say the problem back: the top N customers by the sum of paid amounts."
"I have questions: how big is the file, what counts as paid, and what is a bad row?"
"By hand, the answer for the shop rows is customer 4, then 1, then 3."
"First I write a simple version with Counter and sorted. Then I test it."
"Next I handle bad rows: I skip them, count them, and stop above 20%."
"The time grows with the rows. The memory grows with the customers."
"If the file grows, I stream it. If it is in a database, I use SQL. If it no longer fits one machine, I would think about Spark."
When you are stuck
Say what you know and what you would try next. A quiet pause tells the interviewer nothing. A clear next step shows how you think.
15 · Interview
Timing for 45 minutes
⏱ 4 min · Topic 15 of 19
This is one plan, not a rule. Rounds differ, so ask the interviewer how they use the time. Practise it with a timer:
Minutes
What you do
5
Say the problem back and ask questions.
5
Write examples and edge cases. Work the answer by hand.
15
Write the simple version.
7
Test it on the examples, and fix what fails.
6
Add the bad-row check and the limit.
5
Say the cost, and say how you would scale it.
2
Sum up and ask if they have questions.
The total is 45 minutes. If time runs short, stop adding and finish a correct simple version. A working simple answer is a better place to stop than a half-finished fast one.
16 · Interview
What the Python round looks like
⏱ 8 min · Topic 16 of 19
Many data engineering interview loops have a Python round. Prep guides and candidate reports describe it as live coding or a take-home task.
A live round is often 45 to 60 minutes. Companies differ, so ask the recruiter what your round is.
These are the kinds of problems that people report. The first column is the kind, and the second column gives examples:
Kind of problem
Examples
Warm-up basics
Second largest value. Count a letter. Merge two dicts. First value that appears once.
Data cleaning
Flatten nested JSON. Keep the latest row for each key. Group events into sessions. Skip bad rows and count them.
Light algorithms
Hash map, sliding window, two pointers, intervals, top k with a heap.
Your own project
Walk me through the pipeline on your resume. What happens on a rerun? What if the source changes?
Guides often describe a short warm-up first and one bigger problem second. Do not count on it. The same guides say that the bigger problem looks like real pipeline work more often than like a puzzle contest.
Interviewers are reported to watch for these habits. They come from prep guides, not from official company rules, so use them as a good default:
Say the edge cases before you code: empty input, one row, a missing key, a bad row.
Use plain Python first. Ask before you use pandas.
Write type hints on the function signature.
Use a generator or a single pass when the input is large. Do not call list() on a big input.
Get a correct simple answer first. Make it faster after that.
The same guides say that some topics are asked less often for data engineers. These are trees, linked lists, dynamic programming and backtracking.
Hash maps, strings, arrays, sliding windows and heaps are asked more often. Some companies still ask the rare topics, so check reports for your company before you skip one.
Two questions to ask first
Ask "Can I use pandas?" and "Is the input small, or too big for memory?" The answers change which solution is right.
On this site, the Algorithm patterns path covers the light algorithm problems with data engineering stories. The Python interview questions cover the warm-up basics and the project questions.
17 · Practice
Practice lab
⏱ 6 min · Topic 17 of 19
Each exercise opens in the Python workspace. They are harder than the chapter problem.
In loop week, pick one exercise after you can work the chapter problem in 30 minutes. Set a timer for 20 minutes. Talk aloud as you work.