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.
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.
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.
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 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 10 Programming Fundamentals interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.