Tricky Python Questions Interviewers Love
Part 3 of the Python Interview Prep track. Last updated: October 2026.
Interviewers don't just test what you know — they test what you assume. The questions below are the classics that look trivial and punish overconfidence: identity vs equality, closures, scoping, aliasing, data structures built from scratch, and recursion trade-offs. For each one you'll see the trap most candidates fall into, runnable code proving the real behavior, and the explanation to give in the interview room.
1. is vs ==: identity is not equality
The trap: Most candidates say is and == are interchangeable for values. Then the interviewer runs this:
# == compares VALUES; is compares IDENTITY (same object in memory)
a = 256
b = 256
print(a == b, a is b) # True True — same cached int object
# But build the same number at runtime and it breaks:
x = int("257")
y = int("257")
print(x == y, x is y) # True False — equal value, DIFFERENT objects
s1 = "hello"
s2 = "hello"
print(s1 is s2) # True — short literals are interned (reused)
l1 = [1, 2]
l2 = [1, 2]
print(l1 == l2, l1 is l2) # True False — equal contents, different objects
Explanation: CPython caches small integers (-5 to 256) and interns short strings, so is happens to work for them — but that's an implementation detail, not a language guarantee. Rule: always use == for value comparisons, and is only for singletons like None, True, False. The one-liner to say in the interview: "I use is None because there's exactly one None object."
2. Late binding closures: the famous [2, 2, 2]
The trap: Candidates see a list comprehension of lambdas and predict [0, 1, 2]. The real answer is [2, 2, 2]:
# Each lambda closes over the VARIABLE i — not its value at creation time. funcs = [lambda: i for i in range(3)] print([f() for f in funcs]) # [2, 2, 2] — i is 2 for all of them by call time # The fix: freeze the current value with a default argument. fixed = [lambda i=i: i for i in range(3)] print([f() for f in fixed]) # [0, 1, 2] — each lambda captured its own i
Explanation: This is late binding: the body of a nested function looks up free variables when it's called, not when it's defined. By the time you call the lambdas, the loop has finished and i is 2 everywhere. The default-argument trick works because default values are evaluated once, at definition time. Interviewers love this question because it tests whether you understand Python's scoping rather than memorized syntax.
3. List comprehension scope: the loop variable doesn't leak (but generators are lazy)
The trap: Python 2 veterans (and candidates copying old answers) claim the loop variable of a comprehension leaks into the surrounding scope. In Python 3 it doesn't:
squares = [n * n for n in range(5)]
try:
print(n)
except NameError as e:
print("NameError:", e) # NameError: name 'n' is not defined
# Generator expressions have the same scoping — but are LAZY:
lazy = (n * n for n in range(5))
print(type(lazy)) # <class 'generator'>
print(next(lazy), next(lazy)) # 0 1 — computed on demand
print(list(lazy)) # [4, 9, 16] — only the remaining values!
Explanation: Since Python 3, comprehensions run in their own implicit scope, so the loop variable stays private. Generator expressions additionally compute nothing until consumed — and each value is produced only once, so partially consuming lazy with next() means list(lazy) sees only what's left. Interview line: "Comprehensions don't leak in Python 3, and generators are single-use iterators."
4. The mutable aliasing trap: one object, many names
The trap: Candidates treat assignment as copying. It isn't — it binds another name to the same object:
# a and b point to the SAME list object
a = b = []
a.append(1)
print(a, b) # [1] [1] — "both" lists changed, because there is only one
# The same distinction bites inside functions:
def grow(lst):
lst.append(99) # MUTATES the caller's object
def rebind(lst):
lst = [0, 0] # REBINDS a local name — caller unaffected
data = [1, 2]
grow(data)
print(data) # [1, 2, 99]
rebind(data)
print(data) # [1, 2, 99] — unchanged
Explanation: Python is "pass by object reference": a function receives a reference to the same object the caller passed. Mutating that object (append, item assignment) is visible to the caller; rebinding the parameter name just points a local name elsewhere. This is the root cause of the equally famous mutable-default-argument bug (def f(x=[]) shares one list across all calls). Interview line: "Mutation is visible, rebinding isn't."
5. Stack and Queue from scratch (and why pop(0) is a trap)
The trap: Candidates implement a queue with list.append() and list.pop(0) and call it O(1). It's O(n) — every element shifts one slot left.
# Stack: a plain list is perfect (LIFO — append/pop from the end)
class Stack:
def __init__(self):
self._items = []
def push(self, x):
self._items.append(x) # O(1)
def pop(self):
return self._items.pop() # O(1) — pops from the end
def peek(self):
return self._items[-1]
def is_empty(self):
return not self._items
# Classic use: balanced parentheses
s = Stack()
for token in "((a+b)*(c-d))":
if token == "(":
s.push(token)
elif token == ")":
s.pop()
print("balanced:", s.is_empty()) # balanced: True
# Queue: use collections.deque for TRUE O(1) on both ends
from collections import deque
q = deque()
q.append("a")
q.append("b")
print(q.popleft()) # a — O(1), no shifting
print(list(q)) # ['b']
# What the naive version does instead:
lst = list(range(5))
print(lst.pop(0), lst) # 0 [1, 2, 3, 4] — O(n): every element moves left
# Follow-up interviewers love: a priority queue via heapq (a binary heap)
import heapq
heap = []
for priority, task in [(3, "write report"), (1, "fix bug"), (2, "review PR")]:
heapq.heappush(heap, (priority, task)) # O(log n)
print([heapq.heappop(heap)[1] for _ in range(len(heap))])
# ['fix bug', 'review PR', 'write report']
Explanation: A stack needs only one fast end, so a list's amortized O(1) append/pop is ideal. A queue needs fast operations on both ends — that's exactly what deque (a doubly-linked list of blocks) provides. And when the interviewer asks "what if tasks have priorities?", the answer is heapq: heappush/heappop in O(log n), with the smallest item always on top. Interview line: "Stack = list, queue = deque, priority queue = heapq."
6. Recursion vs iteration: elegance has a price tag
The trap: Candidates write the elegant recursive Fibonacci and never mention it's exponential. This demo is your proof that you understand the cost:
from functools import lru_cache
import sys
calls = 0
def fib_slow(n):
global calls
calls += 1
return n if n < 2 else fib_slow(n - 1) + fib_slow(n - 2)
fib_slow(20)
print("plain recursion calls for fib(20):", calls) # 21891 — exponential!
# Memoize it and the same algorithm turns linear:
@lru_cache(maxsize=None)
def fib_fast(n):
return n if n < 2 else fib_fast(n - 1) + fib_fast(n - 2)
print("lru_cache fib(90):", fib_fast(90)) # 2880067194370816120 — instant
print("recursion limit:", sys.getrecursionlimit()) # usually 1000
# When depth is unbounded or speed matters, prefer iteration:
def factorial_iter(n):
result = 1
for i in range(2, n + 1):
result *= i
return result
print("factorial_iter(5):", factorial_iter(5)) # 120
Explanation: Naive recursion re-solves the same subproblems, so fib(20) explodes to 21,891 calls — O(2^n). lru_cache memoizes each result, collapsing it to O(n): a one-decorator fix every interviewer wants to hear. But memoization doesn't fix depth: Python's recursion limit is ~1000, so deep recursion raises RecursionError where a loop sails through. Interview line: "Recursion for clarity on divide-and-conquer structures like trees; iteration for depth, speed, and tight memory."
7. Bonus: the Big-O cheat sheet and a finally gotcha
The trap: Candidates memorize Big-O labels without knowing which operations trigger them. Keep this table in your head:
# --- Big-O cheat sheet (say these cold) ---
# list.append(x) O(1) — amortized; the dynamic array doubles
# list.insert(0, x) O(n) — shifts everything right
# list.pop() O(1) — from the end
# list.pop(0) O(n) — shifts everything left
# list.sort() O(n log n) — Timsort
# x in list O(n) — linear scan
# dict / set lookup O(1) — average, via hashing
# "k" in "long string" O(n*m) — substring search
# --- The finally gotcha: a return in finally SILENTLY wins ---
def f():
try:
return "from try"
finally:
return "from finally" # this overwrites the try's return!
print(f()) # from finally
Explanation: Interviewers pair Big-O questions with data-structure questions ("which is faster, list or set membership?") — answer O(n) vs O(1) and say why (hashing). The finally twist: finally always runs, and a return inside it replaces any pending return from try/except. Never put a return in finally in real code — reserve it for cleanup like closing files.
Key takeaways
- "Say
==for values,isfor identity — andis Noneonly." - "Closures bind late: the loop variable is looked up at call time, not definition time."
- "Comprehension variables don't leak in Python 3, and generators are single-use."
- "Mutation is visible to the caller; rebinding a parameter name isn't."
- "Stack = list, queue = deque, priority queue = heapq — and
pop(0)is O(n)." - "Naive recursion is exponential;
lru_cachemakes it linear — but watch the 1000-deep recursion limit." - "Never
returninsidefinally— it silently overwrites the real return."
You've completed the Python Interview Prep track! Browse every tutorial on the Python topic page. Next on the roadmap: Python for FDE.
Comments
Post a Comment