Most Python interview lists tell you the answer without telling you what is being tested. That is why people can recite 'tuples are immutable' and still lose the mark: the interviewer wanted the consequence — that immutability buys hashability, which is why a tuple can be a dict key and a list cannot.
The questions below are the ones that keep coming back, and they cluster into five areas: what the built-in types guarantee, how defaults and closures capture values, when identity differs from equality, what the execution model does to threads and memory, and where class-level state leaks between instances. Two of them — the mutable default argument and the loop-of-lambdas — are the same mechanism seen from two directions, which is exactly why interviewers like the pair.
Work through the code before opening the explanation. Each explanation names the follow-up question that usually comes next, because in a real interview the first answer only buys you the right to be asked the harder one.
Five areas, ten problems
Built-in types and when each one breaks
2 problemsInterviewers rarely want the textbook line ('tuples are immutable'). They want to hear the consequence: immutability is what makes tuples hashable, which is what makes them usable as dictionary keys and set members. Answer with the mechanism, then give the case where it matters, and you have said in two sentences what most candidates take a paragraph to circle.
How it gets asked: "What is the difference between a list and a tuple?" · "Why can't you use a list as a dict key?"
Q1
What is the practical difference between a list and a tuple, beyond one being mutable?
- ATuples are always faster, so prefer them everywhere
- BTuples are hashable (if their contents are), so they can be dict keys and set members; lists cannot
- CLists can hold mixed types; tuples cannot
- DTuples cannot be nested inside other containers
▶Show answer & explanation
Answer: B. Tuples are hashable (if their contents are), so they can be dict keys and set members; lists cannot
🐱 Immutability is the cause; hashability is the consequence you can actually use. d[(1, 2)] = 'x' works, d[[1, 2]] = 'x' raises TypeError: unhashable type. Note the caveat that separates a good answer from a great one: a tuple is only hashable if everything inside it is — hash((1, [2])) still fails. Both types hold mixed types, and the speed difference is small enough to be a bad reason to choose.
Q2
What does this print, and why?
a = [1, 2, 3]
b = a[:]
c = a
a.append(4)
print(len(b), len(c))
- A4 4
- B3 4
- C3 3
- D4 3
▶Show answer & explanation
Answer: B. 3 4
🐱 b = a[:] makes a new list (a shallow copy); c = a binds another name to the same object. Appending through a is visible through c but not through b, so the answer is 3 4. The follow-up question is almost guaranteed: a slice copy is shallow, so if the list contained sub-lists, b's sub-lists would still be the same objects as a's — that is when you reach for copy.deepcopy.
Functions, defaults and closures
3 problemsThis group is where interviewers separate people who have used Python from people who have read about it. Two traps come up again and again: default arguments are evaluated once at definition time (so a mutable default is shared across every call), and closures capture variables by reference, not by value (so a lambda created in a loop sees the loop variable's final value). If you can explain both mechanisms and say how to fix each, you have covered most of what gets asked here.
How it gets asked: "What is wrong with def f(x=[])?" · "Explain decorators" · "Why does this loop of lambdas print the same number?"
Q3
What does this print?
def add(item, target=[]):
target.append(item)
return target
print(add(1))
print(add(2))
- A[1] then [2]
- B[1] then [1, 2]
- C[1] then []
- DIt raises a TypeError
▶Show answer & explanation
Answer: B. [1] then [1, 2]
🐱 The default [] is created once, when the def line executes — not on each call. Both calls therefore mutate the same list, giving [1] then [1, 2]. The fix every interviewer wants to hear: default to None and build inside the function (if target is None: target = []). Note this only bites for mutable defaults; def f(x=0) is fine because you cannot mutate an int in place.
Q4
What does this print?
fns = [lambda: i for i in range(3)]
print([f() for f in fns])
- A[0, 1, 2]
- B[2, 2, 2]
- C[3, 3, 3]
- DIt raises a NameError
▶Show answer & explanation
Answer: B. [2, 2, 2]
🐱 Each lambda closes over the variable i, not over its value at creation time. By the time any lambda runs, the comprehension has finished and i is 2, so all three return 2. The standard fix is to bind the value at definition: lambda i=i: i, which evaluates the default immediately — the same 'defaults are evaluated once, at definition' rule from the previous question, used deliberately this time.
Q5
What is a decorator, in one sentence an interviewer will accept?
- AA comment that changes how Python compiles the function
- BA callable that takes a function and returns a replacement, applied with @ syntax
- CA type annotation enforced at runtime
- DA way to make a function run in a separate thread
▶Show answer & explanation
Answer: B. A callable that takes a function and returns a replacement, applied with @ syntax
🐱 @log above def f is exactly f = log(f) — nothing more magical than that. Say the equivalence out loud and the follow-ups become easy: decorators stack bottom-up (the one closest to def wraps first), and the wrapper should carry functools.wraps so the original name and docstring survive. Candidates who only recite 'it adds functionality to a function' usually stall on the stacking question.
Identity, equality and copying
1 problemis compares identity (same object in memory), == compares value. That much everyone says. The mark is won on the second half: any surprising is result you have seen with small integers or short strings comes from CPython interning them as an implementation detail, which means you must never write code that depends on it. Saying 'that behaviour is a CPython implementation detail, not a language guarantee' is the sentence that ends the question well.
How it gets asked: "What is the difference between is and ==?" · "Why does 256 is 256 behave differently from 257 is 257?"
Q6
When should you use is rather than ==?
- AWhenever comparing numbers, because it is faster
- BWhen comparing against None, True or False — the singletons
- CWhen comparing strings, to avoid encoding issues
- DNever;
is is deprecated
▶Show answer & explanation
Answer: B. When comparing against None, True or False — the singletons
🐱 is is the right tool exactly when you mean 'this same object': if x is None is the idiomatic null check, because None is a singleton and a custom class could define __eq__ to make x == None lie. For values, use ==. Using is on numbers or strings appears to work in a REPL because of interning, then fails on the first value that falls outside the cached range — which is the bug this question exists to catch.
Execution model: the GIL and generators
2 problemsThe GIL question is really asking whether you know which of your workloads it hurts. One sentence covers it: the Global Interpreter Lock means only one thread executes Python bytecode at a time, so threads do not speed up CPU-bound work — but they do help I/O-bound work, because the lock is released while waiting on the network or disk. Then name the escape hatch: multiprocessing (or a native extension) for CPU-bound parallelism. Generators are the memory counterpart: they yield one item at a time instead of building the whole list.
How it gets asked: "What is the GIL and how does it affect your code?" · "List comprehension or generator — which and why?"
Q7
Your script spends most of its time waiting on HTTP responses. Will Python threads speed it up?
- ANo — the GIL prevents any concurrency
- BYes — the GIL is released while waiting on I/O, so threads overlap the waiting
- COnly if you disable the GIL at runtime
- DOnly with multiprocessing; threads never help
▶Show answer & explanation
Answer: B. Yes — the GIL is released while waiting on I/O, so threads overlap the waiting
🐱 This is the distinction the question is built around. The GIL serialises bytecode execution, not waiting: while a thread blocks on a socket, it releases the lock and other threads run. So I/O-bound work parallelises fine with threads (or asyncio). Answer (D) is the common over-correction — people hear 'the GIL makes threads useless' and apply it to every workload, including the one where threads are the right tool.
Q8
You need to sum a 10-million-line log file's byte counts. Which is better and why?
# A
total = sum([int(line) for line in f])
# B
total = sum(int(line) for line in f)
- AA — list comprehensions are always faster
- BB — the generator expression avoids building a 10-million-element list in memory
- CThey are identical; Python optimises A into B
- DB is wrong syntax inside sum()
▶Show answer & explanation
Answer: B. B — the generator expression avoids building a 10-million-element list in memory
🐱 A materialises every value into a list first, then sums it — ten million ints held at once. B produces one value at a time, so memory stays flat. The general rule to state: build a list when you need to keep or reuse the items; use a generator when you are only going to iterate once, which is exactly what sum does. Python does not rewrite A into B for you.
Classes: the shared-state trap
1 problemThe class-attribute question is asked because of one specific bug: a mutable class attribute is shared by every instance, so one object's append shows up on all of them. It is the object-oriented cousin of the mutable default argument, and interviewers like it because candidates who memorised the definition still fall into the code version. For the dunder question, the short answer that lands: __repr__ is for developers and should be unambiguous; __str__ is for users and should be readable — and if you only define one, define __repr__, because str() falls back to it.
How it gets asked: "Class attribute vs instance attribute?" · "What is __str__ versus __repr__?"
Q9
What does this print?
class Team:
members = []
def add(self, name):
self.members.append(name)
a, b = Team(), Team()
a.add('Ann')
print(len(b.members))
- A0
- B1
- CIt raises an AttributeError
- D2
▶Show answer & explanation
Answer: B. 1
🐱 members is a class attribute — one list shared by every instance. a.add('Ann') mutates that shared list, so b sees it too and the answer is 1. The fix is to create per-instance state in __init__: def __init__(self): self.members = []. Note the subtlety worth mentioning: rebinding (self.members = [...]) would create an instance attribute and not affect others — it is mutation through the shared reference that leaks.