// Core Language & Data Structures
The Fundamentals Every Interview Assumes You Know Cold
What is the difference between is and ==?
== compares VALUE equality (calls __eq__); is compares IDENTITY — whether two names refer to the literal same object in memory. a == b can be True while a is b is False for two separately-created objects holding equal data. Python caches small integers (-5 to 256) and interned strings, which is why 'a is b' can appear True for small ints even without explicit interning — never rely on this caching behaviour; always use == for value comparison and 'is' only for identity checks (most commonly 'is None').
What is the mutable default argument trap, and why does it happen?
def add_item(item, items=[]): reuses the SAME list object across every call that doesn't explicitly pass items, because default argument values are evaluated exactly once, at function-definition time, not on each call — mutating that shared default in one call leaks into every subsequent call. The fix: use items=None as the default, and create a new list inside the function body when it is None. Covered in depth in the Constructors module, and referenced again in the Closures module since it stems from the same 'defaults evaluated once at definition time' mechanism.
What's the difference between a shallow copy and a deep copy?
A shallow copy (list(original) or original.copy()) creates a new outer container, but any nested mutable objects inside it are still shared references with the original — mutating a nested list inside a shallow copy also mutates the original's nested list. A deep copy (copy.deepcopy()) recursively copies every nested object too, so the copy is fully independent. Reach for deepcopy specifically when the data has nested mutable structures and full independence is required.
Why can't you use a list as a dictionary key, but you can use a tuple?
Dictionary keys must be hashable, and hashability requires the object to be immutable (or at least to have a stable hash for its entire lifetime) — a list is mutable, so Python disallows it as a key entirely (TypeError: unhashable type). A tuple is immutable, so it is hashable, as long as every element it contains is ALSO hashable (a tuple containing a list is itself unhashable).
What is the time complexity of checking membership (x in y) for a list versus a set?
O(n) for a list — Python scans from the start until it finds a match or exhausts the list. O(1) on average for a set or dict, since both use hashing to locate the value directly rather than scanning. This distinction is the single highest-leverage performance fix covered in the Performance module — swapping a list for a set in a membership-heavy loop turns an accidental O(n²) into O(n).
What is the difference between a list comprehension and a generator expression, and when would you choose one over the other?
[x for x in items] builds the entire list in memory immediately. (x for x in items), with parentheses instead of brackets, produces a generator that yields values lazily, one at a time. Choose the comprehension when you need to index into the result, iterate it more than once, or need its length — choose the generator expression when you only need to iterate once and the input could be large, to avoid holding the whole thing in memory at once.
What does *args and **kwargs actually do, and how are they different from each other?
*args collects any extra POSITIONAL arguments into a tuple; **kwargs collects any extra KEYWORD arguments into a dict. Both let a function accept a flexible, unknown-in-advance number of arguments — commonly used for generic wrapper functions (like decorators) that need to forward whatever they were called with to another function, regardless of that function's specific signature.
// Object-Oriented Python
Classes, Inheritance, and Encapsulation
What does super() actually do, and why is it preferred over calling the parent class directly?
super() returns a proxy object that delegates method calls to the next class in the Method Resolution Order (MRO), not necessarily the immediate parent. It correctly supports cooperative multiple inheritance — calling ParentClass.method(self) directly hardcodes a specific class and can skip other classes in a diamond-shaped inheritance hierarchy, while super() respects the full MRO chain regardless of how deep or complex it is.
What is the difference between __str__ and __repr__?
__str__ produces a human-readable description (what print() and f-strings show); __repr__ produces an unambiguous, ideally code-like representation (what the REPL, debuggers, and containers like lists show for their elements). If only one is defined, define __repr__ — Python falls back to it for __str__ automatically, but not the reverse.
Why does defining __eq__ without __hash__ cause an object to behave incorrectly in a set?
Defining __eq__ silently makes the class unhashable by default (Python sets __hash__ to None), UNLESS __hash__ is also explicitly defined — and it must be consistent with __eq__ (equal objects must produce equal hashes), or objects can effectively 'disappear' from sets and dicts, since the hash determines which bucket is checked before __eq__ is ever consulted.
What is the difference between an abstract base class and simply raising NotImplementedError in a base method?
raise NotImplementedError only fails when the specific unimplemented method is actually CALLED — an incomplete subclass can still be instantiated and passed around freely until that line runs. An ABC (abc.ABC + @abstractmethod) fails immediately at INSTANTIATION time if any abstract method is missing, catching the mistake far earlier — often in tests or CI, rather than in production.
What is the difference between @classmethod, @staticmethod, and a regular instance method?
An instance method receives self (the specific object) as its first argument. A classmethod receives cls (the class itself) — commonly used for alternate constructors that correctly respect subclasses. A staticmethod receives neither — it is effectively a plain function namespaced under the class purely for organisation, with no access to instance or class state at all.
How does Python resolve which method actually runs when a class inherits from multiple parents with the same method name?
Via the Method Resolution Order (MRO), computed using the C3 linearisation algorithm — visible by calling ClassName.__mro__ or ClassName.mro(). It produces a single, consistent, left-to-right, depth-first (but de-duplicated) ordering of all ancestor classes, and Python calls the first matching method it finds walking that order. super() respects this same MRO rather than jumping straight to a specific hardcoded parent.
// Functional & Advanced Python
Decorators, Generators, Closures, and Scope
What is a generator, and why use one instead of returning a list?
A generator (written with yield, or as a generator expression) produces values lazily, one at a time, on demand — never holding the entire sequence in memory at once. For a large or even infinite sequence, this is the difference between constant memory usage and potentially exhausting available memory building a full list upfront. The trade-off: a generator can only be iterated once; a list can be iterated repeatedly and indexed directly.
Explain the classic 'closures created in a loop all return the same value' bug.
Closures capture a REFERENCE to the enclosing variable, not a snapshot of its value at each iteration — so several closures created inside the same loop all share the SAME underlying loop variable, which holds its FINAL value by the time any closure is actually called. Fixed by forcing per-iteration capture via a default argument (lambda i=i: i), since default argument values are evaluated immediately at definition time.
What does functools.wraps do, and why does it matter?
Applied to the inner wrapper function inside a decorator, it copies the original function's __name__, __doc__, and other metadata onto the wrapper. Without it, a decorated function's identity (as seen by debuggers, documentation tools, and introspection) silently becomes the wrapper's generic identity instead of the real function's — a subtle but genuinely disruptive loss for tooling.
What is the GIL, and how does it affect the choice between threading and multiprocessing?
The Global Interpreter Lock ensures only one thread executes Python bytecode at a time within a single process, meaning threading does NOT achieve true CPU parallelism for CPU-bound work in standard CPython. Threading is still valuable for I/O-bound work (waiting on network/disk, where the GIL is released during the wait); genuine CPU-bound parallelism requires multiprocessing, which uses separate processes with their own independent GIL and memory space.
What is a context manager, and what problem does the with statement solve that a manual try/finally does not?
A context manager (implementing __enter__/__exit__) guarantees cleanup code runs no matter how a block exits — including via an exception. A manual try/finally solves the same problem, but requires every call site to correctly re-implement the pairing by hand; a reusable context manager centralises correct cleanup in one place, so every caller gets it right automatically with a single 'with' line.
What is the difference between synchronous, threaded, and asyncio-based concurrency in Python, at a high level?
Synchronous code does one thing at a time, blocking on each I/O operation. Threading runs multiple threads that the OS can interleave, useful for I/O-bound work despite the GIL, since it releases during I/O waits. asyncio runs a single thread with a cooperative event loop — coroutines explicitly yield control at await points, giving high-throughput I/O-bound concurrency without the overhead or synchronisation complexity of OS threads, but requiring async-aware libraries throughout the call chain to actually benefit.
// Production, Testing & Typing
What Separates Working Code From Production-Ready Code
Do Python type hints affect runtime behaviour?
No — they are ignored entirely by the interpreter at runtime. A type-annotated function can still be called with mismatched argument types and will run without error. Real enforcement requires a separate static type checker (mypy) run as a development-time/CI step, distinct from actually executing the program.
Why should you generally avoid a bare except: clause?
A bare except: catches EVERY exception, including ones you almost certainly did not intend to swallow — KeyboardInterrupt, SystemExit, and genuine bugs like a NameError from a typo — silently hiding problems that should have been visible. Catch the SPECIFIC exception type(s) you actually expect and know how to handle.
What is the difference between mocking a dependency and using the real one in a test?
Mocking replaces a slow, external, or non-deterministic dependency (an API call, a database, the current time) with a controlled fake for the duration of a test, making the test fast, deterministic, and isolated to the logic actually being verified. Using the real dependency makes tests slower and can introduce flakiness unrelated to the code under test — but over-mocking can leave a test that passes even when the real logic is broken, so the actual logic under test should stay real.
Why is 'with open(file) as f:' preferred over manually calling f.close()?
The with statement (backed by the file object's __enter__/__exit__ methods) guarantees the file is closed no matter how the block exits — normal completion, an early return, or an exception. A manual f.close() call placed after the code that uses the file is simply never reached if an exception occurs first, leaking the file handle.
What is the difference between logging and using print() for diagnostics in production code?
print() always writes to stdout with no way to filter by severity, no timestamps or structured context by default, and no way to redirect output without changing the code. The logging module supports severity levels (DEBUG/INFO/WARNING/ERROR/CRITICAL) that can be filtered per-environment, multiple configurable handlers (console, file, external log aggregation), and structured, consistent formatting — the standard, expected approach for anything beyond a quick throwaway script.
// Hands-On Coding Patterns
Three Classic Problems, Worked Through Completely
Beyond conceptual questions, most technical interviews include at least one live coding problem. These three are among the most commonly seen — not because the exact problems repeat, but because the underlying patterns (hash-map lookups, in-place traversal, simple loop logic) generalise to a huge fraction of what actually gets asked.
1. Two Sum — the canonical hash-map pattern
Given a list of numbers and a target, return the indices of the two numbers that add up to the target. The naive approach checks every pair — O(n²). The optimal approach uses a dict to turn the "have I seen the complement before?" question into an O(1) lookup.
The optimal O(n) solution
def two_sum(nums, target):
seen = {} # value -> index
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return None # no valid pair found
two_sum([2, 7, 11, 15], 9) # [0, 1] — nums[0] + nums[1] == 2 + 7 == 9
The key insight worth saying out loud in an interview: this is a single pass, building the dict as you go, checking each number's complement against numbers already seen before adding the current number — checking before inserting avoids matching a number with itself.
2. Reverse a String In Place — the two-pointer pattern
Given a mutable sequence, reverse it without allocating a second full copy. The two-pointer technique swaps from both ends toward the middle.
In-place reversal with two pointers
def reverse_in_place(chars):
left, right = 0, len(chars) - 1
while left < right:
chars[left], chars[right] = chars[right], chars[left]
left += 1
right -= 1
return chars
reverse_in_place(list("hello")) # ['o', 'l', 'l', 'e', 'h']
Worth noting explicitly: this only works in place on a mutable sequence like a list — Python's actual str type is immutable, so "reversing a string in place" in real Python code is normally just text[::-1], and this exercise exists specifically to demonstrate the two-pointer technique on a mutable structure, a pattern that generalises far beyond strings (checking palindromes, partitioning arrays, and more).
3. FizzBuzz, With the Twist Interviewers Actually Care About
The most famous (and most maligned) interview question is rarely about the logic itself — it's a filter for whether a candidate can translate simple, precise rules into correct code under mild pressure, and increasingly, interviewers extend it with a follow-up to see how a candidate generalises a solution.
Standard FizzBuzz
def fizzbuzz(n):
result = []
for i in range(1, n + 1):
if i % 15 == 0:
result.append("FizzBuzz")
elif i % 3 == 0:
result.append("Fizz")
elif i % 5 == 0:
result.append("Buzz")
else:
result.append(str(i))
return result
The generalised follow-up — an arbitrary set of divisor/word rules
def fizzbuzz_generalised(n, rules):
# rules: a list of (divisor, word) tuples, e.g. [(3, "Fizz"), (5, "Buzz")]
result = []
for i in range(1, n + 1):
word = "".join(w for d, w in rules if i % d == 0)
result.append(word or str(i))
return result
fizzbuzz_generalised(20, [(3, "Fizz"), (5, "Buzz"), (7, "Bazz")])
The generalised version demonstrates something the basic version cannot: recognising that the original problem's hardcoded 3/5/15 logic is really a special case of "check divisibility against an arbitrary list of rules, in order" — exactly the kind of abstraction interviewers are listening for, without over-engineering a problem that did not ask for it.
4. Valid Parentheses — the stack pattern
Given a string of brackets, determine whether every opening bracket has a matching closing bracket in the correct order. This is the canonical example of the stack pattern — appearing constantly in problems involving nested or matched structures (parsing, balanced expressions, undo/redo history).
The stack-based solution
def is_valid(s):
pairs = {")": "(", "]": "[", "}": "{"}
stack = []
for char in s:
if char in pairs.values(): # an opening bracket — push it
stack.append(char)
elif char in pairs: # a closing bracket — must match the top of the stack
if not stack or stack.pop() != pairs[char]:
return False
return not stack # True only if every opening bracket was eventually matched and closed
is_valid("({[]})") # True
is_valid("({[})") # False — the "[" is closed by "}" out of order
is_valid("(") # False — nothing left to match it, stack is non-empty at the end
A Python list, used with only .append() and .pop() (both O(1) at the end of a list), is the idiomatic way to implement a stack — there is no need for a dedicated stack class. The pattern to recognise going forward: any problem involving "does this nested/matched structure resolve correctly" is very often a stack in disguise.
// Misconceptions About Technical Interviews
Four Misconceptions About How Interviews Are Actually Graded
✕ ""The interviewer is mainly grading whether you get the exact optimal solution immediately""
Most interviewers weight the reasoning process — how you approach an unfamiliar problem, whether you consider trade-offs out loud, whether you test your own solution against edge cases — at least as heavily as the final code. A candidate who talks through a brute-force solution first, then improves it, often reads better than one who silently produces an optimal solution with no visible reasoning.
✕ ""Asking clarifying questions before coding makes you look less prepared""
The opposite is generally true — jumping straight into code on an ambiguous problem (unclear input constraints, unclear expected behaviour on edge cases like empty input) is a common red flag, since real engineering work constantly requires clarifying ambiguous requirements before building anything.
✕ ""Interviews mainly test whether you have memorised specific algorithms and data structures""
They mostly test whether you can APPLY a small set of recurring patterns (hash-map lookups, two pointers, basic recursion) to an unfamiliar problem, and whether you can reason clearly about correctness and trade-offs — not whether you have memorised a large catalogue of named algorithms.
✕ ""A single interview question with a wrong final answer means an automatic rejection""
A candidate who reasons well, catches their own mistake, and iterates toward a correct or nearly-correct solution is frequently rated more highly than one who silently produces a "correct" answer with no visible thought process — the process is usually the actual signal being measured, not just the destination.