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.
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.
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.
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.
What is a pure function? Contrast it with a function that has side effects, and give an example of each. Why does purity matter for testability and for safe parallel execution?
Sample Answer
Direct answer
A pure function's output depends only on its inputs, and calling it produces no observable effect outside its own return value, no mutation of external state, no I/O, no reliance on anything that could change between calls. A function with side effects does at least one of those things: it might mutate a variable outside its own scope, write to a file, or return a different result for the same input depending on some external state.
Structured elaboration
- Same input, same output, always: this is the defining property.
math.sqrt(4)is pure, it returns2.0every single time. A function that reads the current time, a global counter, or a mutable default argument that accumulates across calls is not pure, its output can differ across calls even with identical arguments. - No observable effects outside the return value: a pure function doesn't print, doesn't write to a database, doesn't mutate an object passed into it, doesn't increment a counter defined outside itself. If you could delete every call to it (assuming nothing used the return value) and the rest of the program's behavior would be unchanged, that's a strong sign of purity; if deleting a call changes behavior beyond 'the return value is no longer available', something impure happened inside it.
- Why purity matters for testing: a pure function needs no setup beyond its arguments and no teardown, you call it, you assert on the return value, done. An impure function's test has to also arrange whatever external state it reads, and verify whatever external state it mutates, which multiplies both the setup complexity and the number of ways the test can be wrong or flaky.
- Why purity matters for parallel execution: if a function only reads its inputs and produces a return value, calling it concurrently from multiple threads for different inputs is automatically safe, there's no shared mutable state for two calls to race on. An impure function that mutates shared state needs explicit synchronization (locks) to be safe under concurrency, or it will produce wrong results or crashes under load in a way that's notoriously hard to reproduce.
Worked example
def add_tax_pure(price, rate):
return price * (1 + rate) # no side effects
total_calls = {"count": 0}
def add_tax_impure(price, rate):
total_calls["count"] += 1 # side effect: mutates external state
return price * (1 + rate)
Verified: add_tax_pure(100, 0.08) returns 108.0 every time it's called, and calling it twice does not change anything else observable in the program. add_tax_impure(100, 0.08) returns the same 108.0, but after two calls total_calls["count"] has become 2, a change visible to any OTHER code that also reads total_calls, which is exactly the kind of hidden coupling purity avoids: two unrelated pieces of code that both happen to call add_tax_impure now silently affect each other's view of total_calls.
Trade-offs & pitfalls
Purity isn't free, and most real programs need SOME side effects (writing output, updating a database) somewhere; the useful discipline is not 'eliminate all side effects everywhere' but 'push side effects to the edges of the system and keep the core computation (the actual business logic, the actual data transformation) pure', so the large majority of the code gets the testing and concurrency benefits, and the necessarily-impure parts are small, isolated, and easy to reason about individually.
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.