Programming interview questions
Programming questions a Data engineer can be asked, from first job to senior. Open one to see the strongest answer and why it works.
def add(item, bag=[]): bag.append(item); return bag — you call add(1) then add(2). What comes back the second time?
The strongest answer: [1, 2] — the default list is created once when the function is defined, so every call without a bag shares it; use bag=None and build a new list inside
Why it works: Exactly. One default object, shared for the life of the function. None plus a fresh list inside is the fix.
- Default arguments are evaluated once, at definition.
- A mutable default is shared by every call.
- Use None and create the value inside the function.
- The bug is silent, not an error.
When would you choose a tuple over a list?
The strongest answer: When the collection is fixed and its position means something — a coordinate, a record, a dict key — since tuples are immutable and therefore hashable
Why it works: Right. Immutability is the point, and hashability is what it buys you.
- Tuple = fixed, positional, hashable.
- List = a sequence you intend to change.
- Only a tuple can key a dict or join a set.
- Choose on meaning, not on length.
What is the difference between == and is?
The strongest answer: == asks whether two values are equal; is asks whether they are the very same object — use is only for None, True, False and sentinels
Why it works: Exactly. Identity versus equality, and is belongs to singletons.
- == compares values, is compares identity.
- Use is None, not == None.
- Equal objects need not be the same object.
- Caching makes is look right by accident.
You want a value from a dict, or a default if the key is missing, without raising. What do you use?
The strongest answer: d.get(key, default) — or d.setdefault / defaultdict when the missing key should also be stored
Why it works: Right. get for a read; setdefault or defaultdict when the miss should create the entry.
- get(key, default) for a safe read.
- setdefault / defaultdict when the miss should stick.
- in then index looks up twice.
- or swallows falsy stored values.
if items: — what exactly is being tested?
The strongest answer: Whether items is truthy: an empty list, empty string, empty dict, 0 and None are all falsy, so this is not the same as testing for None
Why it works: Exactly — which is why an empty list and None take the same branch unless you separate them.
- Empty containers, 0 and None are all falsy.
- if x is not None when None is the thing you mean.
- if x when any empty value should take the same branch.
- Be explicit when the two differ.
Building a long string in a loop with s += piece is slow on big inputs. Why?
The strongest answer: Strings are immutable, so each += builds a whole new string and copies what came before; collect the pieces in a list and "".join(parts) once
Why it works: Right. Copy per iteration versus one allocation at the end.
- Strings cannot be changed in place.
- += in a loop copies repeatedly.
- Collect into a list, then join.
- The cost grows with the square of the length.
What does xs[1:4] give you, and what does xs[:] do?
The strongest answer: Items at index 1, 2 and 3 — the stop is exclusive — and xs[:] is a shallow copy of the whole list
Why it works: Exactly. Half-open ranges, and a bare slice copies the outer list only.
- Stop is exclusive: [1:4] is 1, 2, 3.
- xs[:] is a shallow copy.
- Slices clamp instead of raising.
- Nested objects are still shared after a shallow copy.
You need both the index and the item while looping. What is the idiomatic way?
The strongest answer: for i, item in enumerate(items) — and enumerate(items, start=1) when the count should begin at one
Why it works: Right, and the start argument saves the off-by-one arithmetic.
- enumerate gives index and item together.
- start=1 for human-facing numbering.
- range(len(x)) is a smell in review.
- Manual counters drift.
When is a list comprehension the right choice over a for loop?
The strongest answer: When you are building one collection from another with a simple expression and filter; once it needs branching, side effects or two levels of nesting, a loop reads better
Why it works: Exactly. It is a readability call, and the line where it flips is nesting.
- Build-from-iterable with a simple expression.
- Side effects belong in a loop.
- Two levels of nesting is the readability limit.
- A generator expression when you do not need the list.
A CSV file loads with the wrong number of columns in some rows. What is usually going on, and how do you read it safely?
The strongest answer: Quoted fields containing commas, quotes or newlines that a naive split does not respect — or a different delimiter or encoding. Use a real CSV parser with declared quoting and encoding, and validate the column count per row
Why it works: Right — CSV is a quoting convention, not a format; a parser that honours it plus per-row validation is the safe read.
- Use a CSV parser, never split.
- Declare quoting and encoding.
- Validate column count per row.
- Quarantine what still fails.
rows2 = rows[:] then rows2[0].append(9). Why did rows change too?
The strongest answer: The slice copied the outer list but not the inner lists, so both lists hold the same row objects — copy.deepcopy, or build new inner lists, if you need them separate
Why it works: Right. One level deep is exactly what a shallow copy means.
- A slice copies one level.
- Inner objects stay shared.
- deepcopy, or rebuild the inner values.
- The same trap applies to dicts of dicts.
What is wrong with except: pass?
The strongest answer: It swallows everything including typos, KeyboardInterrupt and bugs you needed to see; catch the specific exception you can actually handle, and log anything you deliberately ignore
Why it works: Exactly. It hides the failures that would have told you what was wrong.
- Catch the exception you can handle.
- A bare except hides typos and interrupts.
- Log anything you choose to ignore.
- except Exception is broad; bare except is broader.
When does a finally block run?
The strongest answer: On the way out of the try either way — after a successful run, after a handled or unhandled exception, and on return or break — which is why it is where cleanup goes
Why it works: Right. That guarantee is the whole point of the construct.
- finally always runs on the way out.
- except handles, else runs on success.
- Cleanup belongs in finally, or in a with block.
- It runs on return and break too.
You inherit Spark jobs written in Scala. What Scala basics do you need to read them confidently?
The strongest answer: val versus var (immutable by default), case classes as typed records, Option instead of null, and map, filter and flatMap on collections — the same functional shape as the DataFrame API, compiled to the JVM
Why it works: Exactly — those four ideas cover most pipeline code, and knowing it runs in the JVM explains where Scala still wins.
- val, case classes, Option, collections.
- Immutable by default.
- Same functional shape as DataFrames.
- Runs on the JVM.