Common Python Mistakes: 12 Pitfalls Interviewers Watch For
The Python mistakes that fail interviews: mutable default arguments, shared-row grids, late-binding closures, is vs ==, mutating while iterating and more.
- Course: Python study plan
- Module: Interview idioms
- Kind: Lesson
- Reading time: 15 min
- Runtime: CPython 3.11
Why is a mutable default argument a bug in Python?
A mutable default argument is a bug in Python because default values are evaluated once, when the def runs, and stored on the function. Every call that omits the argument shares that same list or dict, so add_item(1) then add_item(2) returns [1, 2]. Default to None instead and create the list inside the function: items = [] if items is None else items.
Lesson
Interviewers keep a short list of Python mistakes, because the same twelve appear in most rounds and each one says something about how well the candidate knows the language. None is exotic; all are one line. This lesson names them, shows each as it appears in practice, explains the mechanism (which is what the follow-up question will ask), and gives the fix. Read it as a checklist to run over your own code before you say "done": mutable defaults, the shared-row grid, late-binding closures, is for values, modifying a list while iterating, .sort() returning None, integer division and modulo with negatives, float equality, the O(n²) string, the recursion limit, and/or returning operands, and the broad except.
1. A mutable default argument
def add_item(item, items=[]): # the list is created once, at def time
items.append(item)
return items
add_item(1); add_item(2) # [1, 2] — shared between calls
Default values are evaluated once when the def runs and stored on the function. Fix: items=None then items = [] if items is None else items. The same applies to {} and to any object created in the default.
2. The shared-row grid
grid = [[0] * 3] * 2 # two references to ONE row
grid[0][0] = 1 # [[1, 0, 0], [1, 0, 0]]
grid = [[0] * 3 for _ in range(2)] # two rows
* on a list repeats references, not copies. Fine for immutables ([0] * 3), wrong for a list of lists.
3. Late-binding closures
fs = [lambda: i for i in range(3)]
[f() for f in fs] # [2, 2, 2] — all see the final i
fs = [lambda i=i: i for i in range(3)] # bind now: [0, 1, 2]
A closure captures the variable, not its value at creation. Fix: a default argument, functools.partial, or a factory function that takes i as a parameter (Module 16).
4. is for values
if x is 5: ... # works by the small-int cache; SyntaxWarning in 3.8+
if s is "done": ... # depends on interning
if x is None: ... # correct: None is a singleton
is is identity. Use == for values and is only for None, True, False and sentinel objects.
5. Modifying a list while iterating over it
for x in xs: # skips elements after each removal
if x % 2 == 0: xs.remove(x)
xs = [x for x in xs if x % 2] # build the new list
for x in xs[:]: ... # or iterate a copy
Removing shifts the remaining elements under the iterator's index. Dicts raise RuntimeError: dictionary changed size during iteration for the same mistake; iterate list(d) or build a new dict.
6. .sort() returns None
xs = xs.sort() # xs is now None
xs.sort() # in place, returns None
ys = sorted(xs) # a new list
Also list.append, list.reverse, dict.update, random.shuffle: in-place methods return None by convention.
7. Integer division and modulo with negatives
-7 // 2 # -4: floors towards negative infinity
-7 % 2 # 1: the result takes the divisor's sign
int(-7 / 2) # -3: truncation, when that is what is meant
Python floors; C and Java truncate. divmod(a, b) returns the consistent pair. For "round half up" use (a + b // 2) // b or decimal.
8. Float equality
0.1 + 0.2 == 0.3 # False
math.isclose(0.1 + 0.2, 0.3) # True
round(2.675, 2) # 2.67 — the float is slightly below 2.675
Compare with a tolerance, format with a fixed precision, or use fractions.Fraction/decimal.Decimal when exactness is the point (money).
9. The O(n²) string
out = ""
for row in rows: out += f"{row}\n" # may copy the growing string each time
out = "\n".join(f"{row}" for row in rows)
CPython optimises some += on strings in place, but not reliably; join is always O(n). Same for repeated list.insert(0, x) and pop(0) (use a deque).
10. The recursion limit
def depth(node): return 1 + max(map(depth, node.children), default=0) # RecursionError at ~1000 deep
sys.setrecursionlimit(1_000_000) # allows it — and iterative BFS/DFS avoids the question
The default limit is 1000 frames; a DFS on a 10⁵-node path exceeds it. Raise the limit, or convert to an explicit stack.
11. and/or return operands, and chained comparisons
name = user_input or "anonymous" # fine: "" → default
count = n and n * 2 # returns 0 when n is 0 — and 0 may not be what you meant
0 < x < 10 # chained: (0 < x) and (x < 10)
x == 1 or 2 # always truthy: (x == 1) or 2
or returns the first truthy operand, and the first falsy one; x in (1, 2) is the fix for the last line.
12. The broad except
try:
value = int(text)
except: # catches KeyboardInterrupt, SystemExit, typos in the try
value = 0
except ValueError: # the one you mean
value = 0
A bare except or except Exception around more than one statement hides the bug that would have explained the failure. Catch the specific exception around the specific statement.
The checklist, before "done"
Defaults immutable? Grid built with a comprehension? Closures bound? == for values? Not mutating what I iterate? sorted where I need the value? Division floors — intended? Floats compared with tolerance? Strings joined? Recursion depth bounded? in instead of == a or b? Exceptions specific? Twelve seconds, twelve points.
Key takeaways
- Defaults and
[[…]] * nshare objects; closures capture variables late; build fresh objects and bind values explicitly. isis identity — only forNoneand sentinels;.sort()and friends returnNone.- Never mutate the list or dict you are iterating; build a new one.
//and%floor; floats neediscloseor fixed formatting; strings are built withjoin.- Raise the recursion limit or go iterative;
orreturns operands; catch specific exceptions around specific statements.
Common questions
Why does [[0] * n] * m create a broken grid?
The outer * repeats a reference to one row, so all m rows are the same list and setting grid[0][0] changes every row. Build the rows with a comprehension instead: [[0] * n for _ in range(m)].
Why does removing items from a list while iterating skip elements?
Each removal shifts the later elements left under the iterator's index, so the element after a removed one is never visited. Build a new list with a comprehension or iterate over a copy; a dict changed during iteration raises RuntimeError instead.
Why does list.sort() return None?
sort works in place, and Python's in-place methods return None by convention, so xs = xs.sort() sets xs to None. Call xs.sort() on its own, or use sorted(xs) for a new list; append, reverse and dict.update behave the same way.
Why is -7 // 2 equal to -4 in Python?
Python's // floors towards negative infinity, and % gives a result with the divisor's sign, so -7 // 2 == -4 and -7 % 2 == 1. C and Java truncate towards zero instead; use int(-7 / 2) when truncation is what you mean.
Why is a bare except bad in Python?
A bare except: catches everything, including KeyboardInterrupt, SystemExit and the typo in the try block that would have explained the failure. Catch the specific exception, such as ValueError, around the specific statement.
Why is x == 1 or 2 always true?
It parses as (x == 1) or 2, and or returns its first truthy operand, so when x is not 1 the result is 2, which is truthy. Write x in (1, 2) to compare against several values.
Exercises
Fix the idioms
The starter is a working program with four bugs from this lesson planted in make_grid, record, make_multipliers and remove_evens: a shared-row grid, a mutable default, late-binding closures and mutation while iterating. Fix all four without changing main. It reads r c and a line of integers, sets grid[0][0] = 1 and prints rows <sum of each row>, calls record once per integer with a fresh history each time and prints history <length of the last one>, applies multipliers 1, 2, 3 to the first integer and prints multipliers <results>, and prints odds <the odd integers> (or odds none).
Input: r c, then the integers. Output: four lines.
2 3
2 2 3 4 4 5
prints
rows 1 0
history 1
multipliers 2 4 6
odds 3 5Predict, then verify
Before running anything, write down what each expression below produces. Then write the program: read expressions one per line until EOF, evaluate each with eval in a namespace containing only math, and print <expression> => <repr of the value> or <expression> raises <exception type name>.
Input: one expression per line. Output: one line per expression.
-7 // 2
-7 % 2
0.1 + 0.2 == 0.3
[1, 2] * 2
"a" * 3
1 < 2 < 3
1 == 1 or 2
[] or "x"
0 and 1
sorted([3, 1, 2]) == [1, 2, 3]
[1, 2, 3].sort()
int("12") + 1
"5" + 5
1 / 0
round(2.5)
prints
-7 // 2 => -4
-7 % 2 => 1
0.1 + 0.2 == 0.3 => False
[1, 2] * 2 => [1, 2, 1, 2]
"a" * 3 => 'aaa'
1 < 2 < 3 => True
1 == 1 or 2 => True
[] or "x" => 'x'
0 and 1 => 0
sorted([3, 1, 2]) == [1, 2, 3] => True
[1, 2, 3].sort() => None
int("12") + 1 => 13
"5" + 5 raises TypeError
1 / 0 raises ZeroDivisionError
round(2.5) => 2In this module: Interview idioms
- The interview template — the round, the file, fast I/O and what is actually judged
- The idiom sheet — the shapes interview problems take and the Python for each
- Pitfalls that fail interviews — the twelve Python mistakes interviewers watch for (this lesson)
- Implement the built-in — a hash map, a dynamic array, an LRU cache and a heap by hand
- Writing clean solutions — structure, names, edges first and the code you can read aloud
- The Python theory drill — thirty questions, thirty answers
- Final checkpoint — Interview idioms
← The idiom sheet — the shapes interview problems take and the Python for each · Implement the built-in — a hash map, a dynamic array, an LRU cache and a heap by hand →