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 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.
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.
Compare the four core built-in container/data types available in most high-level languages (for example Python's list, tuple, set, and dict): describe their mutability, ordering guarantees, typical time complexity for lookup/insert/delete, and when you would reach for each one.
Sample Answer
Direct answer
The four core built-in containers split along two axes: mutability (can you change it after creation?) and whether elements need to be ordered/duplicable versus unique/hashable. A list is a mutable ordered sequence, a tuple is an immutable ordered sequence, a set is a mutable unordered collection of unique hashable elements, and a dict is a mutable unordered mapping of unique hashable keys to values.
Structured elaboration
| Type | Mutable | Ordered | Typical lookup | Typical insert/delete | Use it when |
|---|---|---|---|---|---|
| list | yes | yes (insertion order) | O(n) by value, O(1) by index | O(1) amortized at the end, O(n) at the front/middle | you need an ordered, changeable sequence |
| tuple | no | yes | O(n) by value, O(1) by index | not applicable (immutable) | a fixed-size record, or anything you want to use as a dict key/set member |
| set | yes | no | O(1) average, O(n) worst case | O(1) average, O(n) worst case | fast membership tests, de-duplication |
| dict | yes | yes (insertion order, guaranteed since Python 3.7) | O(1) average, O(n) worst case | O(1) average, O(n) worst case | key-to-value lookup |
The O(1)-average / O(n)-worst-case split for set/dict comes from hashing: normally a hash lookup goes straight to (approximately) the right bucket, but if many keys collide into the same bucket, resolving the collision degenerates toward a linear scan. list/tuple index access is O(1) because the underlying storage is one contiguous block, computing an offset from the index is arithmetic, not a search; searching a list BY VALUE (x in my_list) is O(n) because there's no shortcut, every element may need to be checked.
Worked example
Hashability is the concrete reason tuples, not lists, can be dict keys or set members: {(1, 2): 'a point'} works because a tuple's contents can't change after creation, so its hash value is stable for its lifetime; {[1, 2]: 'a point'} raises TypeError: unhashable type: 'list' because a list's contents CAN change, so Python refuses to let it serve as a hash key at all (verified: t = (1, 2, 3) then t[0] = 99 raises TypeError: 'tuple' object does not support item assignment; {1, 2, 2, 3} == {1, 2, 3}, confirming a set silently drops the duplicate 2).
Trade-offs & pitfalls
The most common mistake is choosing list by default and doing repeated x in my_list membership checks in a hot path, that's O(n) per check and O(n*m) over m checks; switching to a set for membership-only use cases is one of the cheapest performance wins available. The second is using a mutable default in a spot that implicitly needs hashability (trying to use a list as a dict key, or storing lists inside a set) and hitting a TypeError that a tuple would have avoided entirely.
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.
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.