Common Gotchas & Anti-patterns Interview Questions & Answers
7 questions Updated 2026-06-18
Python interview questions on common gotchas: mutable default arguments, late-binding closures, modifying a list while iterating, is vs ==, bare except, mutable class attributes, and shadowing builtins.
A default argument is evaluated once when the function is defined, not on each
call. So a mutable default like [] or {} is created a single time and shared
across every call that doesn't override it — state leaks between calls.
def append_to(item, target=[]): # the SAME list every call
target.append(item)
return target
append_to(1) # [1]
append_to(2) # [1, 2] <- surprise!
def append_to(item, target=None): # the fix: None sentinel
if target is None:
target = [] # fresh list per call
target.append(item)
return target
Use None as the default and create the real object inside the body. This is the
single most famous Python footgun.
Closures in Python capture variables by reference, not by value. When you create functions in a loop, they all close over the same loop variable, which holds its final value by the time they're called.
funcs = [lambda: i for i in range(3)]
[f() for f in funcs] # [2, 2, 2] — not [0, 1, 2]!
# fix: bind the current value via a default argument
funcs = [lambda i=i: i for i in range(3)]
[f() for f in funcs] # [0, 1, 2]
The default-argument trick captures i's value at definition time. (functools. partial works too.) Remember: closures see the variable's latest value, not a
snapshot.
Changing a list's size during iteration shifts the internal index, causing
elements to be skipped or repeated. With dicts and sets it's worse — Python
raises RuntimeError: dictionary changed size during iteration.
nums = [1, 2, 3, 4]
for n in nums:
if n % 2 == 0:
nums.remove(n) # skips elements — buggy
print(nums) # [1, 3] for some inputs, wrong for others
nums = [n for n in nums if n % 2] # build a new list instead
Fix it by iterating over a copy (for n in nums[:]) or, better, building a new
collection with a comprehension or filter. Never mutate a container's size while
looping over it.
is tests identity (same object), while == tests value. CPython caches
small integers (−5 to 256) and interns some strings, so is coincidentally
returns True for those — but fails for values outside the cache, making it look
unreliable.
a = 256; b = 256
a is b # True — cached
c = 1000; d = 1000
c is d # often False — not cached
c == d # True — always correct
x = "hi"; y = "hi"
x is y # True (interned) — don't rely on it
Caching is an implementation detail, not a guarantee. Rule: use == for value
comparison; reserve is for singletons (None, True, False).
A bare except: (or except Exception: used carelessly) catches everything —
including KeyboardInterrupt and SystemExit — and swallows the error silently,
hiding bugs and making programs impossible to interrupt or debug.
try:
risky()
except: # catches EVERYTHING, even Ctrl-C
pass # error vanishes — undebuggable
try:
risky()
except ValueError as e: # catch only what you expect
log.error("bad value: %s", e)
raise # or handle it deliberately
Catch the specific exceptions you can actually handle, and avoid pass in an
except (at minimum log it). If you must catch broadly, use except Exception (not
bare) so system-exiting signals still propagate.
A mutable value assigned at class level (outside __init__) is one object
shared by every instance. Mutating it through any instance affects all of them
— usually not what you intend.
class Cart:
items = [] # SHARED across all instances
def add(self, x):
self.items.append(x)
a, b = Cart(), Cart()
a.add("apple")
b.items # ['apple'] — leaked into b!
class Cart:
def __init__(self):
self.items = [] # per-instance — correct
Initialize mutable attributes inside __init__ so each instance gets its own.
Class-level attributes are fine for immutable constants/defaults, but never for
mutable per-instance state.
Naming a variable after a builtin — list, dict, id, str, type, sum —
shadows it in that scope, so the original becomes unusable and you get confusing
errors later when you try to call it.
list = [1, 2, 3] # shadows the built-in list type
other = list((4, 5)) # TypeError: 'list' object is not callable
id = 42 # now id() is gone
id(other) # TypeError: 'int' object is not callable
Pick non-conflicting names: items/values instead of list, mapping instead of
dict, user_id instead of id. Linters flag builtin shadowing — heed the warning
to avoid these baffling bugs.
Practice tests are coming soon
Get notified when interactive mock interviews and quizzes launch.