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.
Discuss the trade-offs between recursion and iteration: readability, call-stack usage, the risk of a stack overflow on deep input, and tail-call optimization availability across languages. Sketch a recursive factorial implementation and a tail-recursive or iterative variant, and explain why tail-call optimization is not guaranteed even when you write tail-recursive code (for example in Python).
Sample Answer
Direct answer
Recursion trades stack space and a per-call overhead for code that mirrors the problem's natural self-similar structure; iteration trades that clarity for constant stack usage and typically better raw performance. The concrete risk with recursion is a stack overflow on deep input, and the usual mitigating technique, tail-call optimization, is not guaranteed across mainstream languages (notably CPython does not do it).
Structured elaboration
- Readability: recursion often reads closer to the mathematical or structural definition of the problem (a tree, a fractal-like decomposition,
n! = n * (n-1)!). Iteration usually needs an explicit accumulator or work-list and can obscure that structure, especially for tree/graph problems. - Stack usage: each recursive call pushes a new stack frame (return address, local variables). A recursive call chain of depth
nusesO(n)stack space, while a well-written iterative loop usesO(1)auxiliary stack space (the loop variables live in one frame). - Stack overflow risk: if depth exceeds the runtime's limit, you get a hard failure (Python's
RecursionError, a native segfault-style crash in some languages). This is a real production risk whenever recursion depth is driven by input size rather than a small fixed bound. - Tail-call optimization (TCO): in a 'tail-recursive' function, the recursive call is the very last operation, nothing happens after it returns. A compiler or runtime that supports TCO can reuse the current stack frame for that call instead of pushing a new one, turning the recursion into a loop under the hood with
O(1)stack usage. Languages like Scheme and (in the target-relevant case) Java's Scala guarantee this for self-tail-calls; CPython deliberately does NOT implement it (a language design choice, not a limitation of the trick) partly because it would make stack traces less informative for debugging.
Worked example
def factorial_recursive(n):
if n <= 1:
return 1
return n * factorial_recursive(n - 1) # NOT tail-recursive: multiply happens after the call returns
def factorial_tail_style(n, acc=1):
if n <= 1:
return acc
return factorial_tail_style(n - 1, acc * n) # tail-recursive IN FORM, but Python still doesn't optimize it
def factorial_iterative(n):
result = 1
for i in range(2, n + 1):
result *= i
return result
All three agree on small input (verified: factorial_recursive(10) == factorial_iterative(10) == factorial_tail_style(10) == 3628800). The difference shows up at depth: with CPython's default recursion limit of 1000, factorial_recursive(5000) raises RecursionError: maximum recursion depth exceeded (confirmed by running it), while factorial_iterative(5000) completes normally regardless of the tail-style rewrite, because CPython never collapses the recursive call chain into a loop. The same real-world shape shows up walking a deep hierarchical structure (a category tree, a nested comment thread): a recursive walker is elegant until the tree gets deep enough that the recursion limit, not the actual computation, is what fails.
Trade-offs & pitfalls
A correct senior answer does not claim 'just write tail-recursive code and it'll be fine' in a language like Python, that is a common and wrong mental shortcut. The real decision is: if depth is bounded and small (most tree structures in practice), recursion's readability usually wins; if depth scales with untrusted or unbounded input, convert to an explicit iterative version with your own stack (see the tree-traversal conversion question for a worked version of exactly that conversion) rather than relying on the language to save you.
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.
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 the difference between stack and heap memory: what gets allocated where, how variable lifetime differs between the two, and what common pitfalls arise (for example a dangling reference in an unmanaged language, or an object staying reachable longer than intended in a managed one).
Sample Answer
Direct answer
The stack holds each function call's local variables and control-flow bookkeeping in a strict last-in-first-out region that's automatically reclaimed the instant a function returns; the heap holds data whose lifetime isn't tied to any single function call, and it's reclaimed either manually (unmanaged languages) or by a garbage collector (managed languages).
Structured elaboration
- What lives where: a local primitive variable, or in some languages a fixed-size value type, is allocated on the stack as part of the current function's frame. Anything created with an explicit allocation (
newin Java/C++, any Python object, since CPython objects are always heap-allocated even for anint) lives on the heap; the stack only holds a reference/pointer to it. - Variable lifetime: a stack frame's contents die the moment that function returns, this is why you can't return a pointer to a local stack variable in C and expect it to still be valid. Heap objects live until nothing references them anymore, tracked either by the programmer (manual
free/delete), reference counting, or a tracing garbage collector. - Managed vs unmanaged: in C/C++, forgetting to free heap memory is a leak, and freeing it twice or using it after freeing ("use-after-free") is undefined behavior, a classic source of crashes and security bugs. In managed languages like Java or Python, the heap is reclaimed automatically, which removes that class of bug but introduces its own failure mode: an object that's still reachable (through a lingering reference you forgot about) never gets collected even though you're logically done with it, this looks exactly like a leak from the outside even though nothing is 'wrong' with the GC.
- Common pitfalls: dangling references (using a pointer after its target was freed) and double-free in unmanaged languages; unintentional retention (a cache, a global list, or a closure holding a reference longer than intended) in managed languages, which is the managed-language equivalent of a leak.
Worked example
A function def compute(x): result = x * 2; return result allocates result in its stack frame; that frame disappears the instant compute returns. If instead the function does def compute(x): return [x, x*2], the list object itself lives on the heap, only the reference to it lived momentarily in the stack frame, and the list survives the function return because the caller now holds a reference to it. This is exactly why returning a local list is safe in Python (you're returning a heap reference) while returning a pointer to a local stack array in C is not (you're returning a pointer to memory that's about to be reused by the next function call).
Trade-offs & pitfalls
Stack allocation is fast (just moving a pointer) and has zero collection cost; heap allocation is more flexible (variable size, unpredictable lifetime) but costs more per allocation and, in a managed language, imposes collection work (pause time, throughput cost) somewhere down the line. This is a large part of why some languages let you opt certain data onto the stack explicitly (value types, structs) when you know its lifetime is scoped to the current call, to avoid heap/GC overhead for short-lived data.
You find a function that catches every exception and silently returns None on any error (a bare except that swallows the failure). What can go wrong with this pattern in production, and what should replace it? Describe the technical fix (which exceptions to actually catch, how to preserve the failure signal) before considering how you'd raise it with the author.
Sample Answer
Direct answer
A bare except: (or except Exception: with a silent return None) doesn't just handle the error you intended, it catches every error that happens to occur in that block and treats all of them identically, so a genuine bug (wrong type, a typo'd attribute, a logic error) gets misclassified as 'expected failure' and hidden from anyone who could act on it. The fix is to catch only the SPECIFIC exception you actually expect, and make failure visible (log it, re-raise it, or return a value the caller is forced to check) instead of silently returning a value indistinguishable from a normal result.
Structured elaboration
- Why this is dangerous, not just untidy:
Noneis frequently also a valid, meaningful return value elsewhere in the codebase. A caller receivingNonefrom this function cannot tell 'there was no data' from 'something crashed while getting the data', those are very different situations that need very different handling, and the swallowed exception has erased the distinction. - Why 'catch everything' is worse than it looks: a bare
exceptcatchesValueError(probably intended) but ALSOTypeError,AttributeError, evenKeyboardInterruptin some forms, errors that indicate a real bug in the calling code, not a data-quality issue the function was designed to tolerate. Narrowing to the specific expected exception type is what lets a genuine bug surface loudly instead of being absorbed by the same catch-all. - What replaces it: catch only the exception type(s) you actually expect and know how to handle; log enough context to diagnose it (what input caused it); then either re-raise (if the caller has no way to proceed without this data) or return an explicit, unambiguous sentinel that cannot be confused with a valid result (not bare
NoneifNoneis otherwise meaningful). - The review conversation: once the technical fix is clear, raising it with the author is a normal, low-friction code-review comment focused on the concrete failure mode ('this will hide a real TypeError as if it were expected'), not a judgment about the person, that's what keeps the fix landing quickly.
Worked example
def bad_parse(raw):
try:
return int(raw)
except Exception:
return None # catches ValueError AND TypeError identically
def good_parse(raw, logger):
try:
return int(raw)
except ValueError:
logger.warning("could not parse %r as int", raw)
raise # or: return a sentinel the caller is forced to check
Verified: bad_parse(None) returns None silently, giving no signal that int(None) actually raised a TypeError (passing None where a string/number was expected, a real bug at the call site, not a data-quality issue). good_parse(None, logger) instead lets that TypeError propagate uncaught (confirmed: it raises TypeError, not swallowed), because the function only catches ValueError. good_parse("not-a-number", logger) correctly logs a warning and re-raises ValueError (confirmed by execution), a case the function WAS designed to handle, with a visible trail.
Trade-offs & pitfalls
The judgment call is choosing between re-raising and returning a sentinel: re-raise when the caller genuinely cannot proceed without valid data (most cases); return an explicit sentinel only when the caller has a real, intentional fallback path for 'this record was unparseable' and the sentinel can't be confused with a legitimate value. What never belongs in either path is catching a broader exception type than you can actually reason about, that's the pattern that turns a narrow, expected failure mode into a general-purpose bug hiding place.
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.