The questions that actually get asked, with answers that show understanding rather than recall. Each one has a follow-up, because the follow-up is where the interview really happens.
What does this print, and why?
def add(item, basket=[]):
basket.append(item)
return basket
print(add("a")) # Output: ['a']
print(add("b")) # Output: ['a', 'b']
The default is evaluated once, when the def is executed, so every call
relying on it shares one list. The fix is basket=None and building the list inside.
Follow-up: why does Python do that? Because a default is just an expression evaluated at
definition time and stored on the function object — you can see it in
add.__defaults__. Evaluating it per call would mean re-running arbitrary code on every
invocation. The behaviour is consistent; it is only surprising when the value is mutable.
is versus ==
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # Output: True
print(a is b) # Output: False
x = 256
y = 256
print(x is y) # Output: True
== asks whether the values are equal and can be redefined by __eq__.
is asks whether they are the same object and cannot be redefined.
Follow-up: why is 256 is 256 True? CPython pre-allocates the small integers
−5 to 256, so both names point at the same cached object. Try 257 and you may get
False. That is an implementation detail, not a language guarantee, and it is precisely why
you never use is for value comparison — only for None, True and
False.
What is the GIL, and what does it not mean?
The Global Interpreter Lock allows only one thread to execute Python bytecode at a time in a single process. Threads do not give you parallel CPU work in the standard build.
The half that candidates miss: the GIL is released during I/O. A thread waiting on a network call, a disk read or a database query is not holding it. So threads do help with I/O-bound work, and it is only CPU-bound work that gains nothing.
import concurrent.futures, time
def slow_io(n):
time.sleep(0.05) # stands in for a network call
return n
start = time.perf_counter()
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
results = list(pool.map(slow_io, range(4)))
elapsed = time.perf_counter() - start
print(results) # Output: [0, 1, 2, 3]
print(elapsed < 0.15) # Output: True
Four sleeps of 0.05s finishing in well under 0.2s — the GIL was released each time.
Follow-up: so what do you use for CPU work? multiprocessing or
ProcessPoolExecutor — separate processes, each with its own interpreter and its own GIL,
at the cost of pickling data between them. And 3.13 introduced a free-threaded build with no GIL,
officially supported in 3.14, which is changing this answer.
Shallow versus deep copy
import copy
original = {"name": "Alice", "tags": ["a", "b"]}
shallow = copy.copy(original)
deep = copy.deepcopy(original)
shallow["tags"].append("c")
print(original["tags"]) # Output: ['a', 'b', 'c']
print(deep["tags"]) # Output: ['a', 'b']
A shallow copy is a new outer container holding the same inner objects, so mutating
one of those is visible through both. list(x), x.copy() and
x[:] are all shallow.
Follow-up: when would you avoid deepcopy? It is slow, it recurses through the whole object graph, and it fails on things that cannot be copied — an open file, a database connection, a socket. Preferring immutable data is usually the better answer than copying defensively.
Explain decorators
A decorator is a function that takes a function and returns a replacement. @log above a
def means exactly fn = log(fn).
import functools
def log(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
@log
def add(a, b):
"""Add two numbers."""
return a + b
print(add(2, 3)) # Output: 5
print(add.__name__) # Output: add
Follow-up: what does functools.wraps do and why does it matter? It copies the
original's name, docstring and metadata onto the wrapper. Without it the function is called
wrapper in every traceback, debugger and help() — mentioning this
unprompted is the answer that separates people who have written one from people who have read about
them.
Generators versus lists
import sys
squares_list = [n * n for n in range(50_000)]
squares_gen = (n * n for n in range(50_000))
print(sys.getsizeof(squares_list) > 100_000) # Output: True
print(sys.getsizeof(squares_gen) < 250) # Output: True
print(sum(squares_gen) == sum(squares_list)) # Output: True
print(sum(squares_gen)) # Output: 0
The generator stores a position, not results — constant memory whatever the size. That last line is the trap worth volunteering: a generator is consumed once and the second pass gets nothing.
Follow-up: when would you prefer the list? When you need it more than once, need
len(), or need random access. And on small collections the list comprehension is usually
faster, because each yield costs a suspend and resume.
args, kwargs, and mutability
What is the difference between *args and **kwargs? The first
collects extra positional arguments into a tuple, the second extra keyword arguments into a dict. The
stars do the work; the names are convention.
Is Python pass by value or pass by reference? Neither, and saying so is the correct answer: the reference is passed by value. Rebinding a parameter does not affect the caller; mutating the object it points at does.
What makes an object hashable? A stable __hash__ — in practice, immutability.
It is why a tuple can be a dict key and a list cannot.
How to answer when you do not know
Say so, then show how you would find out. "I have not used __slots__ in anger — I know
it avoids the per-instance dict to save memory, and I would check whether it is worth the loss of
dynamic attributes before reaching for it" is a strong answer. Guessing confidently is the weak
one.
Two things that consistently help: think out loud, because the interviewer is assessing your reasoning more than the answer, and volunteer the trade-off. Every question above has one, and naming it unprompted is what distinguishes someone who has used the feature from someone who has memorised it.
And when a question is about something you built, be ready for "why did you do it that way?" — the answer that got you the interview is rarely as interesting as the alternatives you rejected.
That is the end of this track. The basics are in the Python track.