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.
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.
What is a closure, and what does it capture from its enclosing scope? Explain, with a small code example, how a closure or a callback holding a reference can keep an object alive longer than expected (for example through a reference cycle), and describe a practical strategy to avoid or detect that kind of memory retention in a long-running process.
Sample Answer
Direct answer
A closure is a function bundled together with references to the variables from its enclosing scope that it uses, captured by reference (not by value), so it keeps seeing the CURRENT value of those variables even after the enclosing function has returned. That captured reference can create a reference cycle, which is why a closure or callback can keep an object alive longer than you expect.
Structured elaboration
- What gets captured: a closure captures the variable itself (technically, the enclosing scope's cell), not a snapshot of its value at creation time. Two closures created from the same enclosing call share independent state; two closures created from the SAME variable in a loop share the same captured cell, which is the classic 'all my callbacks report the same, final loop value' bug.
- Why closures can leak memory: a closure keeps a live reference to everything it captures for as long as the closure itself is reachable. If you then store that closure back onto an object it captured (a callback registered on the very object it was built from), you've created object -> closure -> object, a reference cycle.
- Why reference counting alone can't free a cycle: CPython's primary memory management is reference counting, an object is freed the instant its reference count hits zero. In a cycle, each object holds a reference to the other, so neither one's count ever reaches zero on its own, even after nothing OUTSIDE the cycle references either of them. This is precisely why CPython also runs a separate cyclic garbage collector (
gcmodule) that periodically looks for groups of objects that reference each other but are unreachable from anywhere else, and frees them as a group. - Mitigation strategies: avoid storing a closure back onto the object it captures when you can restructure to avoid it; use
weakreffor a back-reference that shouldn't keep the target alive (a common pattern for observer/callback registries); or simply trust the cyclic collector for genuinely short-lived cycles and only investigate further if profiling shows real, growing retention in a long-running process.
Worked example
class Node:
def __init__(self, name):
self.name = name
self.on_event = None
def wire(node):
def handler(): # closure: captures `node`
return f"{node.name} handled"
node.on_event = handler # node -> handler -> node : a cycle
return handler
Verified by running it with gc.disable() and a weakref to the node: after del n (dropping the only external reference), the node is STILL alive (ref() is not None is True) because the cycle keeps both objects' reference counts above zero. Re-enabling the collector and calling gc.collect() reclaims it (ref() is None becomes True immediately after), confirming the cyclic collector, not reference counting, is what actually frees this pattern.
Trade-offs & pitfalls
In a long-running service, this usually shows up as slow, steady memory growth rather than an obvious crash, because the cyclic collector DOES eventually run and free most cycles; the real danger is cycles involving objects with a __del__ method (historically these were UNCOLLECTABLE by the cyclic GC before Python 3.4, and even post-3.4 they add real collection overhead) or large cycles that make each collection pass more expensive as the live object graph grows. The fix is rarely 'stop using closures', it's to be deliberate about back-references specifically, using weakref where a callback registry would otherwise hold the only thing keeping a large object graph alive.
Write a function that opens a text file, reads its lines, and returns them as a list of strings, using your language's resource-management construct (for example Java's try-with-resources, or Python's with statement) so the file handle is always closed. Handle a missing file and a permissions error explicitly, and explain why relying on garbage collection to eventually close the handle is not good enough.
Sample Answer
Direct answer
Open the file using your language's scoped resource-management construct (Python's with, Java's try-with-resources), read the lines, and let that construct guarantee the file handle closes when the block exits, whether it exits normally or because an exception was raised partway through, then handle a missing file and a permissions error as distinct, expected outcomes rather than letting either crash the caller.
Structured elaboration
- Why a scoped construct instead of manual open/close: if you close the file with an ordinary line of code after the read, an exception raised during the read skips that line entirely and the handle leaks (file descriptors are a finite OS resource; leaking enough of them eventually breaks the whole process, not just this call).
with/try-with-resources are sugar over exactly the try/finally pattern from the try/except/finally discussion: the close happens in the equivalent of afinallyblock, so it runs on every exit path, success, expected error, or unexpected error. - Handling a missing file: catch the specific exception the platform raises for this (Python's
FileNotFoundError, Java'sNoSuchFileException/FileNotFoundException), and decide deliberately what the caller should see, an empty result, a re-raised application-specific error, or propagation, rather than letting a generic exception surface with no context about which file or why. - Handling a permissions error: similarly catch
PermissionError(Python) / the platform equivalent specifically, this is a genuinely different failure mode from 'file doesn't exist' (the caller might want to alert an operator rather than silently treat it as an empty result) and conflating the two loses information a caller might need to act correctly. - Why relying on garbage collection to eventually close the handle is not good enough: even in a garbage-collected language, GC timing is not guaranteed or immediate, an unclosed handle can sit open for an unpredictable amount of time (or effectively forever, if something keeps a reference alive), during which it holds an OS resource and, for a file opened for writing, may leave buffered data unflushed. The scoped construct closes deterministically at a known point in the code, GC-triggered cleanup does not.
Worked example
def read_lines(path):
try:
with open(path, 'r') as f:
return f.readlines()
except FileNotFoundError:
print(f"file not found: {path}, returning empty list")
return []
except PermissionError:
print(f"permission denied: {path}, returning empty list")
return []
Verified by execution: reading an existing file with lines "line1\n", "line2\n", "line3\n" returns exactly ['line1\n', 'line2\n', 'line3\n']; calling it on a path that doesn't exist returns [] without raising. A separate check confirmed the resource-closing guarantee specifically: wrapping a file object so that reading it raises mid-operation, and confirming the wrapper's close() still ran (wrapper.closed was True) even though the read itself failed, exactly the guarantee with/try-with-resources provides. The equivalent in Java is try (BufferedReader r = new BufferedReader(new FileReader(path))) { ... }, the resource declared in the try (...) parentheses is closed automatically when the block exits, by any path.
Trade-offs & pitfalls
The choice to return an empty list versus re-raising an application-specific exception on a missing/unreadable file is a real design decision, not a default: returning empty silently is convenient for the caller but can hide a real problem (a misconfigured path, a permissions regression) behind what looks like 'the file was just empty'. Whichever you choose, do it deliberately and log enough context (the path, the specific exception) that a missing file and a permissions problem are distinguishable in your logs even if the function's return type can't distinguish them for the caller.
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.
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.
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.