Programming Fundamentals Questions
Language-agnostic building blocks of writing code: variables, primitive and composite data types, scope and lifetime, functions and callbacks, control flow, and expressions versus statements. Covers the mental model a candidate needs before any language-specific or algorithmic depth. The baseline literacy layer of a technical screen.
Explain the difference between a shallow copy and a deep copy. How does plain assignment differ from copying? Walk through what a shallow-copy utility and a deep-copy utility each do to a nested structure (for example a list of lists), and give a concrete example of a bug that a shallow copy of nested/mutable data can silently cause.
Sample Answer
Direct answer
Plain assignment doesn't copy anything, it just gives a second name to the same object. A shallow copy creates a new outer container but reuses references to the same nested objects inside it, so mutating a nested element through either the original or the shallow copy is visible in both. A deep copy recursively copies every nested object too, giving you a fully independent structure.
Structured elaboration
- Assignment (
b = a):aandbare now two names for the exact same object;b is aisTrue. There is no 'original' versus 'copy', they're the same thing. - Shallow copy (
copy.copy(a), orlist(a), ora[:]for a list): creates a genuinely new outer object (b is ais nowFalse), but for every element that is itself a mutable object (a nested list, a dict, a custom object), the copy holds a reference to the SAME nested object, not a copy of it (b[0] is a[0]isTrue). - Deep copy (
copy.deepcopy(a)): recursively walks the structure and makes a new copy of every nested mutable object too, so nothing is shared (b[0] is a[0]isFalse). - The bug shape this causes: code that shallow-copies a nested structure believing it now has an independent snapshot, then mutates the original, and the 'snapshot' silently changes too, because the shallow copy's nested elements were never actually copied.
Worked example
import copy
original = [[1, 2, 3], [4, 5, 6]]
shallow = copy.copy(original)
deep = copy.deepcopy(original)
original[0].append(999) # mutate a NESTED element of the original
Verified results after that mutation: original == [[1, 2, 3, 999], [4, 5, 6]], shallow == [[1, 2, 3, 999], [4, 5, 6]] (the nested list was shared, so the shallow copy sees the change too), deep == [[1, 2, 3], [4, 5, 6]] (fully independent, unaffected).
A realistic version of this bug: code takes a shallow copy of a dataset as a 'before' snapshot, then runs an in-place normalization pass over the dataset:
def normalize_inplace(rows):
for row in rows:
total = sum(row)
for i in range(len(row)):
row[i] = row[i] / total if total else 0
After running normalize_inplace on the dataset, the shallow-copied 'snapshot' taken beforehand is bitwise identical to the now-normalized dataset (verified by running it: snapshot == dataset evaluates True after normalization), because normalize_inplace mutates each row list in place, and the shallow copy's rows are the SAME row objects as the original's. The 'backup' was never a backup.
Trade-offs & pitfalls
The fix depends on what you actually need: if you truly need an independent snapshot, use copy.deepcopy (accepting its cost, see the mutability discussion) or rebuild the structure by copying each nested piece explicitly. If deep-copying every row of a large dataset is too expensive, the more scalable fix is usually to stop mutating in place at all, have normalize_inplace return a new structure instead of mutating its argument, which sidesteps the shallow/deep copy question entirely by removing the shared-mutable-state pattern that created the risk.
A recursive function that does an in-order traversal of a binary tree raises a stack-overflow/recursion-depth error on deep trees. Convert it to an explicit iterative version (using your own stack data structure) that yields nodes in the same in-order sequence. Provide a code sketch and explain how the iterative approach avoids the recursion-depth limit while preserving traversal order.
Sample Answer
Direct answer
Convert the recursive walk into an explicit loop that maintains its own stack (a plain list/array), pushing left children as you descend and popping/visiting/moving right exactly where the recursive calls would have happened, so the traversal order is identical but the call depth is no longer tied to the language's function-call stack.
Structured elaboration
The recursive in-order traversal is: recurse left, visit the node, recurse right. Each recursive call pushes a real stack frame, so a left-skewed tree of depth d uses O(d) frames and blows the interpreter's recursion limit once d exceeds it (Python's default is 1000). The iterative version replaces those implicit call-stack frames with an explicit stack you manage yourself, which lives on the heap and has no language-imposed depth limit (bounded only by available memory, not by a fixed call-depth ceiling):
- Walk left as far as possible, pushing every node visited onto the stack (mirrors descending through the left-recursion calls without visiting yet).
- Pop the top of the stack, that's the next node to visit, in the exact order the recursive version would have visited it.
- Move to that node's right child and repeat from step 1 (mirrors the recurse-right call).
- Stop when the stack is empty and there's no current node left to descend into.
Worked example
def inorder_iterative(root):
out = []
stack = []
node = root
while stack or node is not None:
while node is not None:
stack.append(node)
node = node.left
node = stack.pop()
out.append(node.val)
node = node.right
return out
Verified against a known small balanced tree (root 4, left subtree 2 with children 1 and 3, right subtree 6 with children 5 and 7): both the recursive version and this iterative version return [1, 2, 3, 4, 5, 6, 7], identical order. Verified against a deliberately pathological case, a left-skewed tree of depth 3000 (each node's left child is the next node down, no right children): the recursive version raises RecursionError: maximum recursion depth exceeded at Python's default limit of 1000, while the iterative version completes and returns all 3000 values in order (len(result) == 3000, result == list(range(1, 3001))), confirming it has no equivalent depth ceiling.
Trade-offs & pitfalls
The iterative version is not simply 'better', it trades the recursive version's direct correspondence to the problem's structure (which makes it easy to verify by inspection) for an explicit stack whose invariant (everything on the stack is an ancestor of the current node, still awaiting its visit-and-descend-right) is easy to get subtly wrong, common bugs are popping before fully descending left, or forgetting to move to node.right after visiting and looping forever on the same node. This conversion is worth doing specifically when input depth is attacker- or user-controlled and therefore cannot be assumed small (a request-driven tree/graph walk), not as a blanket 'recursion is bad' rule for every tree operation.
Explain mutability versus immutability: what makes an object immutable, and what are the performance and safety trade-offs? Give examples in one or two languages of your choice, discuss how immutability helps with concurrent/multithreaded correctness, and describe when immutability itself can become a performance problem.
Sample Answer
Direct answer
An immutable object's state can never change after construction, any operation that looks like a modification actually produces a new object; a mutable object's state can be changed in place through the same reference. The trade-off is safety and reasoning simplicity (immutable) versus avoided copying and lower memory churn (mutable).
Structured elaboration
- What immutability buys you: once you hold a reference to an immutable object, nobody else holding a different reference to the SAME object can surprise you by changing it out from under you. This matters most where a value is shared: as a dict/set key (see the containers discussion), as a default function argument, and across threads.
- Why it helps concurrency specifically: a data race requires at least one thread to write while another reads (or writes) the same memory. If the object literally cannot be written to after construction, that half of the race is structurally impossible, so immutable data can be freely shared across threads with zero synchronization (no locks needed) for reads. This is a much stronger guarantee than 'we were careful with locking'.
- What it costs: every 'modification' allocates a new object and (for anything nontrivial) copies the parts that didn't logically change. For a small string this is free; for a large data structure updated in a tight loop, allocating a full new copy per update can dominate runtime and memory traffic, this is the performance problem immutability can become.
- Java's concrete example:
Stringis immutable, every apparent concatenation makes a newStringobject; repeatedly concatenating in a loop is a classic O(n^2) performance trap for exactly this reason, which is whyStringBuilder(a mutable, purpose-built accumulator) exists as the escape hatch. Python'stuplevslistis the same shape:tuplegives you the sharing-safety and hashability of immutability,listgives you cheap in-place growth when you know you own the only reference.
Worked example
name = "engineer"
upper_name = name.upper() # returns a NEW string; name itself is untouched
assert name == "engineer"
assert upper_name == "ENGINEER"
nums = [1, 2, 3]
nums.append(4) # mutates the SAME list object in place
assert nums == [1, 2, 3, 4]
(both assertions verified). name.upper() cannot change name because Python strings are immutable, there is no operation that mutates a str in place; nums.append(4) changes the exact object nums refers to, so any other variable that also referenced that list would see the appended 4 too, that aliasing behavior is the concrete risk mutable shared state introduces.
Trade-offs & pitfalls
The practical decision is rarely 'immutability is always better', it's 'default to immutable for anything shared or used as a key, and reach for mutable structures deliberately, in the narrow scope where you know you own the only reference and the update pattern is hot enough that copy-on-write would actually cost something measurable'. Treating one choice as universally correct, in either direction, is the mistake.
Explain what happens mechanically when you write a try/except/finally block (or the equivalent in your language): what runs, in what order, when no exception occurs, when one is raised and caught, and when one is raised and NOT caught. Then walk through handling a file-I/O error inside a function that opens a file, using the construct to guarantee the file is always closed even when an error occurs.
Sample Answer
Direct answer
try marks a block whose exceptions you want to intercept; except runs only if a matching exception is raised inside the try; finally always runs, whether or not an exception occurred and whether or not it was caught, and it runs even if the exception propagates past this block entirely.
Structured elaboration
- No exception: the
tryblock runs to completion, everyexceptclause is skipped, thenfinallyruns. Nothing unusual happens. - Exception raised and caught: execution jumps out of the
tryblock the instant the exception is raised (any code after the raising line intrydoes NOT run), the first matchingexceptclause runs, thenfinallyruns. - Exception raised and NOT caught (no
exceptclause matches its type): the matching-exceptsearch fails,finallystill runs (this is the part people get wrong:finallyis not conditional on being caught), and only afterfinallycompletes does the exception continue propagating up to the caller. - The consequence that matters in practice:
finallyis the correct place for resource cleanup that must happen unconditionally, closing a file handle, releasing a lock, rolling back a partially-started operation, precisely because it is the one block guaranteed to run on every exit path from thetry.
Worked example
def no_exception():
order = []
try:
order.append('try')
except ValueError:
order.append('except')
finally:
order.append('finally')
return order
def caught_exception():
order = []
try:
order.append('try')
raise ValueError('boom')
except ValueError:
order.append('except')
finally:
order.append('finally')
return order
Running both (verified): no_exception() returns ['try', 'finally'] (the except clause never runs), caught_exception() returns ['try', 'except', 'finally']. A third case with a TypeError raised where only ValueError is caught confirms finally still runs before the TypeError propagates out to the caller, exactly as described above.
For the file-I/O case, opening a file and guaranteeing it closes even on a mid-read failure:
def read_lines(path):
f = open(path, 'r')
try:
return f.readlines()
finally:
f.close() # runs whether readlines() succeeds, raises, or the caller's except re-raises
This is exactly what a with open(path) as f: block (or Java's try-with-resources) does under the hood, they are sugar over a try/finally that closes the resource. In this pattern, whether to re-raise the error after logging it or return a sentinel value to the caller depends on whether the caller can meaningfully continue: propagate (let the exception continue, possibly after logging) when the caller cannot proceed without the data; return a sentinel only when the caller has a genuine, documented fallback.
Trade-offs & pitfalls
The most common mechanical mistake is assuming finally only runs when an exception was caught, when in fact it runs on every exit from try (normal completion, caught exception, uncaught exception, even a return inside the try block). The second most common mistake is putting cleanup code after the try/except instead of in finally, which silently skips cleanup on any path that doesn't hit that exact line, exactly the bug that finally exists to prevent.
What is a pure function? Contrast it with a function that has side effects, and give an example of each. Why does purity matter for testability and for safe parallel execution?
Sample Answer
Direct answer
A pure function's output depends only on its inputs, and calling it produces no observable effect outside its own return value, no mutation of external state, no I/O, no reliance on anything that could change between calls. A function with side effects does at least one of those things: it might mutate a variable outside its own scope, write to a file, or return a different result for the same input depending on some external state.
Structured elaboration
- Same input, same output, always: this is the defining property.
math.sqrt(4)is pure, it returns2.0every single time. A function that reads the current time, a global counter, or a mutable default argument that accumulates across calls is not pure, its output can differ across calls even with identical arguments. - No observable effects outside the return value: a pure function doesn't print, doesn't write to a database, doesn't mutate an object passed into it, doesn't increment a counter defined outside itself. If you could delete every call to it (assuming nothing used the return value) and the rest of the program's behavior would be unchanged, that's a strong sign of purity; if deleting a call changes behavior beyond 'the return value is no longer available', something impure happened inside it.
- Why purity matters for testing: a pure function needs no setup beyond its arguments and no teardown, you call it, you assert on the return value, done. An impure function's test has to also arrange whatever external state it reads, and verify whatever external state it mutates, which multiplies both the setup complexity and the number of ways the test can be wrong or flaky.
- Why purity matters for parallel execution: if a function only reads its inputs and produces a return value, calling it concurrently from multiple threads for different inputs is automatically safe, there's no shared mutable state for two calls to race on. An impure function that mutates shared state needs explicit synchronization (locks) to be safe under concurrency, or it will produce wrong results or crashes under load in a way that's notoriously hard to reproduce.
Worked example
def add_tax_pure(price, rate):
return price * (1 + rate) # no side effects
total_calls = {"count": 0}
def add_tax_impure(price, rate):
total_calls["count"] += 1 # side effect: mutates external state
return price * (1 + rate)
Verified: add_tax_pure(100, 0.08) returns 108.0 every time it's called, and calling it twice does not change anything else observable in the program. add_tax_impure(100, 0.08) returns the same 108.0, but after two calls total_calls["count"] has become 2, a change visible to any OTHER code that also reads total_calls, which is exactly the kind of hidden coupling purity avoids: two unrelated pieces of code that both happen to call add_tax_impure now silently affect each other's view of total_calls.
Trade-offs & pitfalls
Purity isn't free, and most real programs need SOME side effects (writing output, updating a database) somewhere; the useful discipline is not 'eliminate all side effects everywhere' but 'push side effects to the edges of the system and keep the core computation (the actual business logic, the actual data transformation) pure', so the large majority of the code gets the testing and concurrency benefits, and the necessarily-impure parts are small, isolated, and easy to reason about individually.
Unlock Full Question Bank
Get access to all 12 Programming Fundamentals interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.