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.
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.
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.
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.
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.
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.
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.