These are the classic “What’s the output?” questions that come up in Python interviews. Each one tests a specific gotcha. Let’s walk through them.
1. Mutable Default Argument
def add_item(item, lst=[]):
lst.append(item)
return lst
print(add_item("a"))
print(add_item("b"))
Output: ['a'] then ['a', 'b']
The default list [] is created once when the function is defined, not on each call. Every call that uses the default shares the same list object. Fix: use lst=None and create a new list inside the function.
2. Late Binding Closures
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])
Output: [2, 2, 2]
Closures capture the variable, not the value. By the time we call the lambdas, the loop is done and i is 2. Fix: use a default argument to capture the current value: lambda i=i: i.
3. Integer Caching
a = 256
b = 256
print(a is b)
c = 257
d = 257
print(c is d)
Output: True then False (in the standard REPL)
Python caches integers from -5 to 256 for performance. So a and b point to the same object. Numbers outside that range create new objects each time. This is an implementation detail of CPython — never rely on is for number comparisons.
4. String Interning
a = "hello"
b = "hello"
print(a is b)
c = "hello world"
d = "hello world"
print(c is d)
Output: True then False (typically)
Python automatically interns (reuses) strings that look like identifiers — no spaces, simple characters. "hello" gets interned, "hello world" might not. Like integer caching, this is a CPython optimization. Always use == for string comparison, never is.
5. List Multiplication Gotcha
grid = [[0]] * 3
grid[0][0] = 5
print(grid)
Output: [[5], [5], [5]]
The * operator doesn’t create three separate lists. It creates three references to the same inner list. Changing one changes all of them. Fix: use a list comprehension: [[0] for _ in range(3)].
6. Exception Variable Scope
try:
raise ValueError("oops")
except ValueError as e:
error = e
print(type(error))
try:
print(e)
except NameError:
print("e is gone!")
Output: <class 'ValueError'> then e is gone!
The variable e is deleted after the except block exits. This is by design — it prevents reference cycles with the traceback. But if we assign it to another name (like error), that reference survives.
7. Tuple with Mutable Element
t = ([1, 2],)
t[0].append(3)
print(t)
Output: ([1, 2, 3],)
Wait, tuples are immutable! Yes, but the tuple holds a reference to the list, and the reference doesn’t change. We’re not reassigning t[0] — we’re mutating the list it points to. The tuple itself is unchanged (same reference), but the list inside it grew.
8. Chained Comparisons
print(1 < 2 < 3)
print(1 < 2 > 0)
print(3 > 2 > 3)
Output: True, True, False
Python chains comparisons. 1 < 2 < 3 becomes 1 < 2 and 2 < 3. Same idea: 1 < 2 > 0 becomes 1 < 2 and 2 > 0, which is True and True. This is different from most languages where 1 < 2 > 0 would evaluate left-to-right as True > 0.
9. is vs == with Lists
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b)
print(a is b)
print(a is c)
Output: True, False, True
== checks if the values are equal. is checks if they’re the same object in memory. a and b have the same content but are two different list objects. c = a doesn’t copy — it creates another reference to the same object. Think of is as “are these the same box?” and == as “do these boxes contain the same stuff?”
In simple language, most of these gotchas come down to understanding the difference between objects and references, and knowing that Python reuses objects in surprising ways for performance.