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.
Define the four pillars of object-oriented programming: encapsulation, abstraction, inheritance, and polymorphism. For each, give a short, concrete example in a language of your choice, explain one practical benefit it provides, and name one common pitfall or misuse you have seen.
Sample Answer
Direct answer
The four pillars are encapsulation (bundling data with the methods that operate on it, and hiding internal state behind a controlled interface), abstraction (exposing only what a caller needs, hiding how it's implemented), inheritance (a class reusing and specializing another class's behavior), and polymorphism (code that works against a common interface behaving correctly for many concrete types).
Structured elaboration
- Encapsulation: internal fields are kept private (or convention-marked, e.g. Python's leading underscore) and reached only through methods. Benefit: you can change the internal representation later without breaking every caller. Pitfall: exposing a mutable internal collection directly (a public list field) defeats encapsulation even if the field itself is 'private', because callers can still reach in and mutate it.
- Abstraction: a caller depends on a small, stable surface (an interface, an abstract base class, or just a documented method contract) rather than on implementation detail. Benefit: implementations can be swapped freely. Pitfall: a 'leaky abstraction' that forces callers to know implementation detail anyway (e.g. an interface that only makes sense if you know it's backed by a SQL table).
- Inheritance: a subclass gets a base class's fields and methods and can override behavior. Benefit: real code reuse for a genuine is-a relationship. Pitfall: inheritance used purely for code reuse (not a real is-a relationship) creates fragile coupling, since a change in the base class can silently break every subclass; composition (a class holding an instance of another class instead of inheriting from it) is often the safer default for anything beyond a shallow, genuinely-is-a hierarchy.
- Polymorphism: calling code written against a base type or interface automatically gets the right behavior for whatever concrete subtype is actually passed in, without an if/else on type. Benefit: adding a new type means adding a new class, not editing every call site (this is most of what the 'open/closed' principle (a design should be open to new behavior but closed to editing existing, working code) is about). Pitfall: relying on type-checking (
isinstance) instead of polymorphism reintroduces the very branching the pattern exists to remove.
Worked example
class MetricCollector:
def __init__(self, name):
self._name = name # encapsulation: internal state, reached via methods
self._samples = []
def record(self, value):
self._samples.append(value)
def summary(self):
return f"{self._name}: n={len(self._samples)}"
class LatencyCollector(MetricCollector): # inheritance
def summary(self): # polymorphism: overrides base behavior
if not self._samples:
return f"{self._name}: no samples"
avg = sum(self._samples) / len(self._samples)
return f"{self._name}: avg={avg:.2f}ms over {len(self._samples)} samples"
Calling .summary() on a plain MetricCollector gives "requests: n=2"; calling the exact same method name on a LatencyCollector after recording 12.0 and 18.0 gives "p50_latency: avg=15.00ms over 2 samples" (verified: (12.0 + 18.0) / 2 = 15.00). Code that only knows it has a MetricCollector and calls .summary() gets the right behavior either way, that's the polymorphism.
Trade-offs & pitfalls
The pillars are not equally load-bearing in modern code: encapsulation and abstraction are used constantly and rarely controversial, while deep inheritance hierarchies are increasingly avoided in favor of composition ('composition over inheritance') once a hierarchy goes past one or two levels, because each additional layer makes behavior harder to predict from any single class definition. A senior answer should name that tension rather than presenting a 4-item inheritance-friendly hierarchy as the goal in itself.
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.
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.
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.
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.
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.